diff --git a/README.md b/README.md index d7e3ea57f..4033766c8 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,13 @@ There are currently 2 ways to run the application: 1. As an executable 1. deployed in a Java Servlet 3.0 container +Note that some features require encoded slashes in the URL. In tomcat (which is embedded in the war), this can be +allowed with: + +``` +-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true +``` + ### Running as an executable `java -jar shibui.war` diff --git a/backend/build.gradle b/backend/build.gradle index 98b40151b..2d1a7ba43 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -56,7 +56,7 @@ dependencies { } // spring boot auto-config starters - ['starter-web', 'starter-data-jpa', 'starter-security', 'starter-actuator', 'devtools'].each { + ['starter-web', 'starter-data-jpa', 'starter-security', 'starter-actuator', 'devtools', 'starter-webflux'].each { compile "org.springframework.boot:spring-boot-${it}" } // TODO: figure out what this should really be @@ -87,6 +87,8 @@ dependencies { testCompile "org.xmlunit:xmlunit-core:2.5.1" testRuntime 'cglib:cglib-nodep:3.2.5' + testCompile "net.shibboleth.ext:spring-extensions:5.4.0-SNAPSHOT" + //JSON schema generator testCompile 'com.kjetland:mbknor-jackson-jsonschema_2.12:1.0.29' testCompile 'javax.validation:validation-api:2.0.1.Final' 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 2aec22834..386e37dea 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 @@ -4,9 +4,8 @@ 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.scheduled.EntityDescriptorFilesScheduledTasks; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; -import edu.internet2.tier.shibboleth.admin.ui.service.EntityIdsSearchService; -import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl; +import edu.internet2.tier.shibboleth.admin.ui.service.*; +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; @@ -22,7 +21,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.util.UrlPathHelper; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -39,9 +42,34 @@ public OpenSamlObjects openSamlObjects() { return new OpenSamlObjects(); } + @Bean + public EntityService jpaEntityService() { + return new JPAEntityServiceImpl(openSamlObjects()); + } + @Bean public EntityDescriptorService jpaEntityDescriptorService() { - return new JPAEntityDescriptorServiceImpl(openSamlObjects()); + return new JPAEntityDescriptorServiceImpl(openSamlObjects(), jpaEntityService()); + } + + @Bean + public FilterService jpaFilterService() { + return new JPAFilterServiceImpl(); + } + + @Bean + public FilterTargetService jpaFilterTargetService() { + return new JPAFilterTargetServiceImpl(); + } + + @Bean + public MetadataResolverService metadataResolverService() { + return new JPAMetadataResolverServiceImpl(); + } + + @Bean + public AttributeUtility attributeUtility() { + return new AttributeUtility(); } @Autowired @@ -63,7 +91,7 @@ public EntityIdsSearchService entityIdsSearchService() { try { IndexSearcher searcher = new IndexSearcher(DirectoryReader.open(directory)); QueryParser parser = new QueryParser("content", fullTokenAnalyzer); - TopDocs topDocs = searcher.search(parser.parse(term), limit); + TopDocs topDocs = searcher.search(parser.parse(term.trim()), limit); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { Document document = searcher.doc(scoreDoc.doc); entityIds.add(document.get("id")); @@ -74,4 +102,43 @@ public EntityIdsSearchService entityIdsSearchService() { return new EntityIdsSearchResultRepresentation(entityIds); }; } + + /** + * A WebMvcConfigurer that won't mangle the path for the entities endpoint. + * + * inspired by [ https://stackoverflow.com/questions/13482020/encoded-slash-2f-with-spring-requestmapping-path-param-gives-http-400 ] + * + * @return configurer + */ + @Bean + public WebMvcConfigurer webMvcConfigurer() { + return new WebMvcConfigurer() { + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + UrlPathHelper helper = new UrlPathHelper() { + @Override + public String getServletPath(HttpServletRequest request) { + String servletPath = getOriginatingServletPath(request); + if (servletPath.startsWith("/api/entities")) { + return servletPath; + } else { + return super.getOriginatingServletPath(request); + } + } + + @Override + public String getOriginatingServletPath(HttpServletRequest request) { + String servletPath = request.getRequestURI().substring(request.getContextPath().length()); + if (servletPath.startsWith("/api/entities")) { + return servletPath; + } else { + return super.getOriginatingServletPath(request); + } + } + }; + helper.setUrlDecode(false); + configurer.setUrlPathHelper(helper); + } + }; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java index 7158a6f9b..f4b3036ee 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.resolver.ResolverException; import org.apache.http.impl.client.HttpClients; @@ -9,6 +10,7 @@ import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; +import org.joda.time.DateTime; import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver; import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.opensaml.saml.metadata.resolver.impl.FileBackedHTTPMetadataResolver; @@ -18,7 +20,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import javax.annotation.Nullable; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * this is a temporary class until a better way of doing this is found. @@ -33,10 +38,17 @@ public class MetadataResolverConfiguration { @Autowired IndexWriter indexWriter; + @Autowired + MetadataResolverRepository metadataResolverRepository; + @Bean public MetadataResolver metadataResolver() throws ResolverException, ComponentInitializationException { - MetadataResolver metadataResolver = new ChainingMetadataResolver(); + ChainingMetadataResolver metadataResolver = new ChainingMetadataResolver(); + metadataResolver.setId("chain"); + List resolvers = new ArrayList<>(); + + // TODO: remove this later when we allow for creation of arbitrary metadata resolvers FileBackedHTTPMetadataResolver incommonMR = new FileBackedHTTPMetadataResolver(HttpClients.createMinimal(), "http://md.incommon.org/InCommon/InCommon-metadata.xml", "/tmp/incommon.xml"){ @Override protected void initMetadataResolver() throws ComponentInitializationException { @@ -58,11 +70,27 @@ protected void initMetadataResolver() throws ComponentInitializationException { throw new ComponentInitializationException(e); } } + + @Nullable + @Override + public DateTime getLastRefresh() { + return null; + } }; incommonMR.setId("incommonmd"); incommonMR.setParserPool(openSamlObjects.getParserPool()); incommonMR.initialize(); + resolvers.add(incommonMR); + + if (!metadataResolverRepository.findAll().iterator().hasNext()) { + edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver mr = new edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver(); + mr.setName("incommonmd"); + metadataResolverRepository.save(mr); + } + + metadataResolver.setResolvers(resolvers); + metadataResolver.initialize(); return metadataResolver; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java index fe59e92ba..4ef76087f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/WebSecurityConfig.java @@ -5,11 +5,19 @@ import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.firewall.HttpFirewall; +import org.springframework.security.web.firewall.StrictHttpFirewall; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +/** + * Web security configuration. + * + * Workaround for slashes in URL from [https://stackoverflow.com/questions/48453980/spring-5-0-3-requestrejectedexception-the-request-was-rejected-because-the-url] + */ @EnableWebSecurity public class WebSecurityConfig { @@ -19,6 +27,13 @@ public class WebSecurityConfig { @Value("${shibui.default-password:}") private String defaultPassword; + @Bean + public HttpFirewall allowUrlEncodedSlashHttpFirewall() { + StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowUrlEncodedSlash(true); + return firewall; + } + @Bean @Profile("default") public WebSecurityConfigurerAdapter defaultAuth() { @@ -48,6 +63,12 @@ protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); } } + + @Override + public void configure(WebSecurity web) throws Exception { + super.configure(web); + web.httpFirewall(allowUrlEncodedSlashHttpFirewall()); + } }; } @@ -60,6 +81,12 @@ protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.headers().frameOptions().disable(); } + + @Override + public void configure(WebSecurity web) throws Exception { + super.configure(web); + web.httpFirewall(allowUrlEncodedSlashHttpFirewall()); + } }; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesController.java new file mode 100644 index 000000000..8a5172fb6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesController.java @@ -0,0 +1,68 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.resolver.ResolverException; +import org.opensaml.core.criterion.EntityIdCriterion; +import org.opensaml.core.xml.io.MarshallingException; +import org.opensaml.saml.metadata.resolver.MetadataResolver; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import javax.servlet.http.HttpServletRequest; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +@Controller +@RequestMapping(value = "/api/entities", method = RequestMethod.GET) +public class EntitiesController { + private static final Logger logger = LoggerFactory.getLogger(EntitiesController.class); + + @Autowired + private MetadataResolver metadataResolver; + + @Autowired + private EntityDescriptorService entityDescriptorService; + + @Autowired + private OpenSamlObjects openSamlObjects; + + @RequestMapping(value = "{entityId:.*}") + public ResponseEntity getOne(final @PathVariable String entityId, HttpServletRequest request) throws UnsupportedEncodingException, ResolverException { + EntityDescriptor entityDescriptor = this.getEntityDescriptor(entityId); + if (entityDescriptor == null) { + return ResponseEntity.notFound().build(); + } + EntityDescriptorRepresentation entityDescriptorRepresentation = entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor); + return ResponseEntity.ok(entityDescriptorRepresentation); + } + + @RequestMapping(value = "{entityId:.*}", produces = "application/xml") + public ResponseEntity getOneXml(final @PathVariable String entityId) throws MarshallingException, ResolverException, UnsupportedEncodingException { + EntityDescriptor entityDescriptor = this.getEntityDescriptor(entityId); + if (entityDescriptor == null) { + return ResponseEntity.notFound().build(); + } + final String xml = this.openSamlObjects.marshalToXmlString(entityDescriptor); + return ResponseEntity.ok(xml); + } + + private EntityDescriptor getEntityDescriptor(final String entityId) throws ResolverException, UnsupportedEncodingException { + String decodedEntityId = URLDecoder.decode(entityId, "UTF-8"); + EntityDescriptor entityDescriptor = this.metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(decodedEntityId))); + // TODO: we need to clean this up sometime + if (entityDescriptor instanceof edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor) { + ((edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor) entityDescriptor).setResourceId(null); + } + return entityDescriptor; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index 8f0d83cd9..3e9780607 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -83,7 +83,7 @@ public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam return handleUploadingEntityDescriptorXml(xmlContents, spName); } catch (Throwable e) { LOGGER.error("Error fetching XML metadata from the provided URL: [{}]. The error is: {}", metadataUrl, e); - e.printStackTrace(); + LOGGER.error(e.getMessage(), e); return ResponseEntity .badRequest() .body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage())); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FilterController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FilterController.java new file mode 100644 index 000000000..a818de149 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FilterController.java @@ -0,0 +1,127 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/MetadataResolver/{metadataResolverId}") +public class FilterController { + + private static Logger LOGGER = LoggerFactory.getLogger(FilterController.class); + + @Autowired + private MetadataResolverRepository repository; + + @Autowired + private FilterService filterService; + + @Autowired + private MetadataResolverService metadataResolverService; + + @GetMapping("/Filters") + @Transactional(readOnly = true) + public Iterable getAll(@PathVariable String metadataResolverId) { + // TODO: implement lookup based on metadataResolverId once we have more than one + return repository.findAll().iterator().next().getMetadataFilters().stream() + .map(eaf -> filterService.createRepresentationFromFilter((EntityAttributesFilter) eaf)) + .collect(Collectors.toList()); + } + + @GetMapping("/Filter/{resourceId}") + public ResponseEntity getOne(@PathVariable String metadataResolverId, @PathVariable String resourceId) { + // TODO: implement lookup based on metadataResolverId once we have more than one + // TODO: should we check that we found exactly one filter (as in the update method below)? If not, error? + return ResponseEntity.ok(repository.findAll().iterator().next().getMetadataFilters().stream() + .filter(eaf -> eaf.getResourceId().equals(resourceId)) + .map(eaf -> filterService.createRepresentationFromFilter((EntityAttributesFilter) eaf)) + .collect(Collectors.toList()).get(0)); + } + + @PostMapping("/Filter") + public ResponseEntity create(@PathVariable String metadataResolverId, @RequestBody FilterRepresentation filterRepresentation) { + //TODO: replace with get by metadataResolverId once we have more than one + MetadataResolver metadataResolver = repository.findAll().iterator().next(); + + List filterList = (List)(List) metadataResolver.getMetadataFilters(); // bleh. casting. + EntityAttributesFilter createdFilter = filterService.createFilterFromRepresentation(filterRepresentation); + filterList.add(createdFilter); + + metadataResolver.setMetadataFilters((List)(List) filterList); + MetadataResolver persistedMr = repository.save(metadataResolver); + + // we reload the filters here after save + metadataResolverService.reloadFilters(persistedMr.getName()); + + return ResponseEntity + .created(getResourceUriFor(persistedMr, createdFilter.getResourceId())) + .body(filterService.createRepresentationFromFilter( + (EntityAttributesFilter) + persistedMr.getMetadataFilters() + .stream() + .filter(filter -> filter.getResourceId().equals(createdFilter.getResourceId())) + .collect(Collectors.toList()) + .get(0) // "There can be only one!!!" + ) + ); + } + + @PutMapping("/Filter/{resourceId}") + public ResponseEntity update(@PathVariable String metadataResolverId, @RequestBody FilterRepresentation filterRepresentation) { + //TODO: replace with get by metadataResolverId once we have more than one + MetadataResolver metadataResolver = repository.findAll().iterator().next(); + + List filters = (List)(List) + metadataResolver.getMetadataFilters().stream() + .filter(eaf -> eaf.getResourceId().equals(filterRepresentation.getId())) + .collect(Collectors.toList()); + if (filters.size() != 1) { + // TODO: I don't think this should ever happen, but... if it does... + // do something? throw exception, return error? + LOGGER.warn("More than one filter was found for id {}! This is probably a bad thing.\n" + + "We're going to go ahead and use the first one, but .. look in to this!", filterRepresentation.getId()); + } + + EntityAttributesFilter eaf = filters.get(0); + // convert our representation so we can get the attributes more easily... + EntityAttributesFilter updatedFilter = filterService.createFilterFromRepresentation(filterRepresentation); + eaf.setName(updatedFilter.getName()); + eaf.setFilterEnabled(updatedFilter.isFilterEnabled()); + eaf.setEntityAttributesFilterTarget(updatedFilter.getEntityAttributesFilterTarget()); + eaf.setAttributes(updatedFilter.getAttributes()); + + MetadataResolver persistedMr = repository.save(metadataResolver); + + metadataResolverService.reloadFilters(persistedMr.getName()); + + return ResponseEntity.ok() + .body(filterService.createRepresentationFromFilter((EntityAttributesFilter)persistedMr.getMetadataFilters().stream() + .filter(filter -> filter.getResourceId().equals(filterRepresentation.getId())) + .collect(Collectors.toList()).get(0))); + } + + private static URI getResourceUriFor(MetadataResolver mr, String filterResourceId) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/MetadataResolver/") + .pathSegment(mr.getResourceId()) + .pathSegment("Filter") + .pathSegment(filterResourceId) + .build() + .toUri(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java index e68f3ae23..d1cf5a4ea 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AbstractDescriptor.java @@ -17,9 +17,6 @@ @MappedSuperclass public abstract class AbstractDescriptor extends AbstractAttributeExtensibleXMLObject implements CacheableSAMLObject, TimeBoundSAMLObject, SignableXMLObject { - - private boolean isValid; - private Long cacheDuration; @Type(type = "org.jadira.usertype.dateandtime.joda.PersistentDateTime") @@ -34,11 +31,10 @@ public abstract class AbstractDescriptor extends AbstractAttributeExtensibleXMLO @Override public boolean isValid() { - return isValid; - } - - public void setIsValid(boolean isValid) { - this.isValid = isValid; + if (null == validUntil) { + return true; + } + return new DateTime().isBefore(this.validUntil); } @Override diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java index eb70609d5..e3eb808e5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/Attribute.java @@ -75,4 +75,14 @@ public List getOrderedChildren() { return Collections.unmodifiableList(children); } + + @Override + public String toString() { + return "Attribute{" + + "name='" + name + "\'\n" + + ", nameFormat='" + nameFormat + "\'\n" + + ", friendlyName='" + friendlyName + "\'\n" + + ", attributeValues=" + attributeValues + + "\n}"; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilter.java new file mode 100644 index 000000000..d673ab4b8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilter.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.google.common.base.Predicate; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.OneToOne; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +public class EntityAttributesFilter extends MetadataFilter { + @OneToOne(cascade = CascadeType.ALL) + private EntityAttributesFilterTarget entityAttributesFilterTarget; + + @OneToMany(cascade = CascadeType.ALL) + private List attributes = new ArrayList<>(); + + public EntityAttributesFilterTarget getEntityAttributesFilterTarget() { + return entityAttributesFilterTarget; + } + + public void setEntityAttributesFilterTarget(EntityAttributesFilterTarget entityAttributesFilterTarget) { + this.entityAttributesFilterTarget = entityAttributesFilterTarget; + } + + public List getAttributes() { + return attributes; + } + + public void setAttributes(List attributes) { + this.attributes = attributes; + } + + @Override + public String toString() { + return "EntityAttributesFilter{" + + "entityAttributesFilterTarget=" + entityAttributesFilterTarget + + "\n, attributes=" + attributes + + "\n}"; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTarget.java new file mode 100644 index 000000000..bbd79f811 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTarget.java @@ -0,0 +1,61 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.internet2.tier.shibboleth.admin.ui.controller.FilterController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import javax.persistence.Transient; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class EntityAttributesFilterTarget extends AbstractAuditable { + public enum EntityAttributesFilterTargetType { + ENTITY, CONDITION_SCRIPT, CONDITION_REF + } + + private static Logger LOGGER = LoggerFactory.getLogger(EntityAttributesFilterTarget.class); + + private EntityAttributesFilterTargetType entityAttributesFilterTargetType; + + @ElementCollection + private List value; + + public EntityAttributesFilterTargetType getEntityAttributesFilterTargetType() { + return entityAttributesFilterTargetType; + } + + public void setEntityAttributesFilterTargetType(EntityAttributesFilterTargetType entityAttributesFilterTarget) { + this.entityAttributesFilterTargetType = entityAttributesFilterTarget; + } + + public List getValue() { + return value; + } + + public void setValue(String value) { + List values = new ArrayList<>(); + values.add(value); + this.value = values; + } + + public void setValue(List value) { + this.value = value; + } + + @Override + public String toString() { + return "EntityAttributesFilterTarget{" + + "entityAttributesFilterTargetType=" + entityAttributesFilterTargetType + + ", value=" + value + + '}'; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataFilter.java new file mode 100644 index 000000000..f5b7c11bd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataFilter.java @@ -0,0 +1,40 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.Column; +import javax.persistence.Entity; +import java.util.UUID; + +/** + * Domain class to store information about {@link org.opensaml.saml.metadata.resolver.filter.MetadataFilter} + */ +@Entity +public class MetadataFilter extends AbstractAuditable { + private String name; + @Column(unique=true) + private String resourceId = UUID.randomUUID().toString(); + private boolean filterEnabled; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public boolean isFilterEnabled() { + return filterEnabled; + } + + public void setFilterEnabled(boolean filterEnabled) { + this.filterEnabled = filterEnabled; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataResolver.java new file mode 100644 index 000000000..5ec51e147 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataResolver.java @@ -0,0 +1,50 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +public class MetadataResolver extends AbstractAuditable { + private String name; + private String resourceId = UUID.randomUUID().toString(); + + @OneToMany(cascade = CascadeType.ALL) + private List metadataFilters = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public List getMetadataFilters() { + return metadataFilters; + } + + public void setMetadataFilters(List metadataFilters) { + this.metadataFilters = metadataFilters; + } + + @Override + public String toString() { + return "MetadataResolver{\n" + + "name='" + name + "\'\n" + + ", resourceId='" + resourceId + "\'\n" + + ", metadataFilters=\n" + metadataFilters + + '}'; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java index ab3f88cb8..369f10719 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/XSBoolean.java @@ -29,4 +29,11 @@ public String getStoredValue() { public void setStoredValue(String storedValue) { this.storedValue = storedValue; } + + @Override + public String toString() { + return "XSBoolean{" + + "storedValue='" + storedValue + '\'' + + '}'; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterRepresentation.java new file mode 100644 index 000000000..ce0bc26f8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterRepresentation.java @@ -0,0 +1,90 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +public class FilterRepresentation implements Serializable { + private String id; + private String filterName; + private boolean filterEnabled; + private FilterTargetRepresentation filterTarget; + private RelyingPartyOverridesRepresentation relyingPartyOverrides; + private List attributeRelease; + private LocalDateTime createdDate; + private LocalDateTime modifiedDate; + + public FilterRepresentation() { + + } + + public FilterRepresentation(String id, String filterName, boolean filterEnabled) { + this.id = id; + this.filterName = filterName; + this.filterEnabled = filterEnabled; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFilterName() { + return filterName; + } + + public void setFilterName(String filterName) { + this.filterName = filterName; + } + + public boolean isFilterEnabled() { + return filterEnabled; + } + + public void setFilterEnabled(boolean filterEnabled) { + this.filterEnabled = filterEnabled; + } + + public FilterTargetRepresentation getFilterTarget() { + return filterTarget; + } + + public void setFilterTarget(FilterTargetRepresentation filterTarget) { + this.filterTarget = filterTarget; + } + + public RelyingPartyOverridesRepresentation getRelyingPartyOverrides() { + return relyingPartyOverrides; + } + + public void setRelyingPartyOverrides(RelyingPartyOverridesRepresentation relyingPartyOverrides) { + this.relyingPartyOverrides = relyingPartyOverrides; + } + + public List getAttributeRelease() { + return attributeRelease; + } + + public void setAttributeRelease(List attributeRelease) { + this.attributeRelease = attributeRelease; + } + + public LocalDateTime getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(LocalDateTime createdDate) { + this.createdDate = createdDate; + } + + public LocalDateTime getModifiedDate() { + return modifiedDate; + } + + public void setModifiedDate(LocalDateTime modifiedDate) { + this.modifiedDate = modifiedDate; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterTargetRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterTargetRepresentation.java new file mode 100644 index 000000000..773178cf0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/FilterTargetRepresentation.java @@ -0,0 +1,51 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import edu.internet2.tier.shibboleth.admin.util.FilterTargetRepresentationDeserializer; + +import java.util.ArrayList; +import java.util.List; + +@JsonDeserialize(using = FilterTargetRepresentationDeserializer.class) +public class FilterTargetRepresentation { + private String type; + private List value; + + public FilterTargetRepresentation() { + + } + + public FilterTargetRepresentation(String type, String value) { + this.type = type; + List values = new ArrayList<>(); + values.add(value); + this.value = values; + } + + public FilterTargetRepresentation(String type, List listValue) { + this.type = type; + this.value = listValue; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public void setValue(String value) { + List values = new ArrayList<>(); + values.add(value); + this.value = values; + } + + public List getValue() { + return value; + } + + public void setValue(List listValue) { + this.value = listValue; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java index e431c1ea9..21cc6262c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/opensaml/OpenSamlObjects.java @@ -16,11 +16,14 @@ import org.opensaml.core.xml.io.Unmarshaller; import org.opensaml.core.xml.io.UnmarshallerFactory; import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import javax.annotation.PostConstruct; import javax.xml.namespace.QName; +import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; @@ -31,6 +34,7 @@ import java.io.StringWriter; public class OpenSamlObjects { + Logger logger = LoggerFactory.getLogger(OpenSamlObjects.class); private XMLObjectBuilderFactory builderFactory; @@ -88,10 +92,12 @@ public String marshalToXmlString(XMLObject ed) throws MarshallingException { if (marshaller != null) { try (StringWriter writer = new StringWriter()) { Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); transformer.transform(new DOMSource(marshaller.marshall(ed)), new StreamResult(writer)); entityDescriptorXmlString = writer.toString(); } catch (TransformerException | IOException e) { - e.printStackTrace(); + logger.error(e.getMessage(), e); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepository.java new file mode 100644 index 000000000..827a2cd99 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepository.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter; +import org.springframework.data.repository.CrudRepository; + +public interface FilterRepository extends CrudRepository { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java new file mode 100644 index 000000000..5dba01872 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java @@ -0,0 +1,11 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver; +import org.springframework.data.repository.CrudRepository; + +/** + * Repository to manage {@link edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver} instances. + */ +public interface MetadataResolverRepository extends CrudRepository { + MetadataResolver findByName(String name); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index 65f5971e0..086c65f64 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -1,8 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.service; +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import java.util.List; + /** * Main backend facade API that defines operations pertaining to manipulating {@link EntityDescriptor} state. * @@ -33,4 +37,21 @@ public interface EntityDescriptorService { * @param representation front end representation to use to update */ void updateDescriptorFromRepresentation(final EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); -} + + /** + * Given a list of attributes, generate an AttributeReleaseList + * + * @param attributeList the list of attributes to convert + * @return an AttributeRelease list + */ + List getAttributeReleaseListFromAttributeList(List attributeList); + + /** + * Given a list of attributes, generate a RelyingPartyOverridesRepresentation + * + * @param attributeList the list of attributes to generate from + * @return a RelyingPartyOverridesRepresentation based on the given list of attributes + */ + RelyingPartyOverridesRepresentation getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java new file mode 100644 index 000000000..fbb85f6d2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java @@ -0,0 +1,16 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; +import org.opensaml.saml.saml2.core.Attribute; + +import java.util.List; + +/** + * facade API that defines operations for creating various entities from JSON representations + */ +public interface EntityService { + List getAttributeListFromEntityRepresentation(EntityDescriptorRepresentation entityDescriptorRepresentation); + List getAttributeListFromAttributeReleaseList(List attributeReleaseList); + List getAttributeListFromRelyingPartyOverridesRepresentation(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java new file mode 100644 index 000000000..4b72affa8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterService.java @@ -0,0 +1,28 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; + +/** + * Main backend facade API that defines operations pertaining to manipulating {@link EntityAttributesFilter} objects. + * + * @since 1.0 + */ +public interface FilterService { + + /** + * Map from front-end data representation of attributes filter entity attributes filter model + * + * @param representation of attributes filter coming from front end layer + * @return EntityAttributesFilter + */ + EntityAttributesFilter createFilterFromRepresentation(final FilterRepresentation representation); + + /** + * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor + * + * @param entityAttributesFilter + * @return FilterRepresentation front end representation + */ + FilterRepresentation createRepresentationFromFilter(final EntityAttributesFilter entityAttributesFilter); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterTargetService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterTargetService.java new file mode 100644 index 000000000..259077e8a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/FilterTargetService.java @@ -0,0 +1,29 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation; + +/** + * Main backend facade API that defines operations pertaining to manipulating {@link EntityAttributesFilter} objects. + * + * @since 1.0 + */ +public interface FilterTargetService { + + /** + * Map from front-end data representation of Filter Target to Entity Attributes Filter Target + * + * @param representation of attributes filter coming from front end layer + * @return EntityAttributesFilterTarget' + */ + EntityAttributesFilterTarget createFilterTargetFromRepresentation(final FilterTargetRepresentation representation); + + /** + * Map from Entity Attributes Filter Target to front-end data representation + * + * @param entityAttributesFilterTarget + * @return FilterTargetRepresentation front end representation + */ + FilterTargetRepresentation createRepresentationFromFilterTarget(final EntityAttributesFilterTarget entityAttributesFilterTarget) ; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 56255c995..922f94df7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -68,11 +68,15 @@ public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private OpenSamlObjects openSamlObjects; + @Autowired + private EntityService entityService; + public JPAEntityDescriptorServiceImpl() { } - public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects) { + public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects, EntityService entityService) { this.openSamlObjects = openSamlObjects; + this.entityService = entityService; } @Override @@ -231,53 +235,7 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto } if (representation.getRelyingPartyOverrides() != null || (representation.getAttributeRelease() != null && representation.getAttributeRelease().size() > 0)) { - if (representation.getRelyingPartyOverrides() != null) { - // Let's do the overrides - RelyingPartyOverridesRepresentation overridesRepresentation = representation.getRelyingPartyOverrides(); - if (overridesRepresentation.isSignAssertion()) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)); - } - if (overridesRepresentation.isDontSignResponse()) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)); - } - if (overridesRepresentation.isTurnOffEncryption()) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)); - } - if (overridesRepresentation.isUseSha()) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")); - } - if (overridesRepresentation.isIgnoreAuthenticationMethod()) { - // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate - // TODO: fix - getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")); - } - if (overridesRepresentation.isOmitNotBefore()) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)); - } - if (overridesRepresentation.getResponderId() != null && !"".equals(overridesRepresentation.getResponderId())) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, overridesRepresentation.getResponderId())); - } - if (overridesRepresentation.getNameIdFormats() != null && overridesRepresentation.getNameIdFormats().size() > 0) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, overridesRepresentation.getNameIdFormats())); - } - if (overridesRepresentation.getAuthenticationMethods() != null && overridesRepresentation.getAuthenticationMethods().size() > 0) { - getEntityAttributes(ed).getAttributes().add(createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, overridesRepresentation.getAuthenticationMethods())); - } - } - - // let's map the attribute release - if (representation.getAttributeRelease() != null && representation.getAttributeRelease().size() > 0) { - Attribute attribute = ((AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); - getEntityAttributes(ed).addAttribute(attribute); - - attribute.setName(MDDCConstants.RELEASE_ATTRIBUTES); - - for (String attributeRelease : representation.getAttributeRelease()) { - XSString xsString = (XSString) openSamlObjects.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); - xsString.setValue(attributeRelease); - attribute.getAttributeValues().add(xsString); - } - } + getEntityAttributes(ed).getAttributes().addAll(entityService.getAttributeListFromEntityRepresentation(representation)); } return ed; } @@ -578,6 +536,69 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope return representation; } + @Override + public List getAttributeReleaseListFromAttributeList(List attributeList) { + List releaseAttributes = attributeList.stream() + .filter(attribute -> attribute.getName().equals(MDDCConstants.RELEASE_ATTRIBUTES)) + .collect(Collectors.toList()); + + if (releaseAttributes.size() != 1) { + // TODO: What do we do if there is more than one? + } + if (releaseAttributes.size() == 0) { + return new ArrayList<>(); + } else { + return getStringListOfAttributeValues(releaseAttributes.get(0).getAttributeValues()); + } + } + + @Override + public RelyingPartyOverridesRepresentation getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { + RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation = new RelyingPartyOverridesRepresentation(); + + for (org.opensaml.saml.saml2.core.Attribute attribute : attributeList) { + Attribute jpaAttribute = (Attribute) attribute; + // TODO: this is going to get real ugly real quick. clean it up, future Jj! + switch (jpaAttribute.getName()) { + case MDDCConstants.SIGN_ASSERTIONS: + relyingPartyOverridesRepresentation.setSignAssertion(getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.SIGN_RESPONSES: + relyingPartyOverridesRepresentation.setDontSignResponse(!getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.ENCRYPT_ASSERTIONS: + relyingPartyOverridesRepresentation.setTurnOffEncryption(!getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.SECURITY_CONFIGURATION: + if (getStringListValueOfAttribute(jpaAttribute).contains("shibboleth.SecurityConfiguration.SHA1")) { + relyingPartyOverridesRepresentation.setUseSha(true); + } + break; + case MDDCConstants.DISALLOWED_FEATURES: + if ((Integer.decode(getStringListValueOfAttribute(jpaAttribute).get(0)) & 0x1) == 0x1) { + relyingPartyOverridesRepresentation.setIgnoreAuthenticationMethod(true); + } + break; + case MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE: + relyingPartyOverridesRepresentation.setOmitNotBefore(!getBooleanValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.RESPONDER_ID: + relyingPartyOverridesRepresentation.setResponderId(getStringListValueOfAttribute(jpaAttribute).get(0)); + break; + case MDDCConstants.NAME_ID_FORMAT_PRECEDENCE: + relyingPartyOverridesRepresentation.setNameIdFormats(getStringListValueOfAttribute(jpaAttribute)); + break; + case MDDCConstants.DEFAULT_AUTHENTICATION_METHODS: + relyingPartyOverridesRepresentation.setAuthenticationMethods(getStringListValueOfAttribute(jpaAttribute)); + break; + default: + break; + } + } + + return relyingPartyOverridesRepresentation; + } + private boolean getBooleanValueOfAttribute(Attribute attribute) { return ((XSBoolean) attribute.getAttributeValues().get(0)).getValue().getValue(); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java new file mode 100644 index 000000000..ff5c3981a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java @@ -0,0 +1,136 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder; +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; +import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; +import org.opensaml.saml.saml2.core.Attribute; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +public class JPAEntityServiceImpl implements EntityService { + + @Autowired + private OpenSamlObjects openSamlObjects; + + @Autowired + private AttributeUtility attributeUtility; + + public JPAEntityServiceImpl() {} + + public JPAEntityServiceImpl(OpenSamlObjects openSamlObjects) { + this.openSamlObjects = openSamlObjects; + } + + @Override + public List getAttributeListFromEntityRepresentation(EntityDescriptorRepresentation entityDescriptorRepresentation) { + List list = new ArrayList<>(); + if (entityDescriptorRepresentation.getRelyingPartyOverrides() != null) { + // Let's do the overrides + RelyingPartyOverridesRepresentation overridesRepresentation = entityDescriptorRepresentation.getRelyingPartyOverrides(); + if (overridesRepresentation.isSignAssertion()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)); + } + if (overridesRepresentation.isDontSignResponse()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)); + } + if (overridesRepresentation.isTurnOffEncryption()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)); + } + if (overridesRepresentation.isUseSha()) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")); + } + if (overridesRepresentation.isIgnoreAuthenticationMethod()) { + // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate + // TODO: fix + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")); + } + if (overridesRepresentation.isOmitNotBefore()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)); + } + if (overridesRepresentation.getResponderId() != null && !"".equals(overridesRepresentation.getResponderId())) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, overridesRepresentation.getResponderId())); + } + if (overridesRepresentation.getNameIdFormats() != null && overridesRepresentation.getNameIdFormats().size() > 0) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, overridesRepresentation.getNameIdFormats())); + } + if (overridesRepresentation.getAuthenticationMethods() != null && overridesRepresentation.getAuthenticationMethods().size() > 0) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, overridesRepresentation.getAuthenticationMethods())); + } + } + + // let's map the attribute release + if (entityDescriptorRepresentation.getAttributeRelease() != null && entityDescriptorRepresentation.getAttributeRelease().size() > 0) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + list.add(attribute); + + attribute.setName(MDDCConstants.RELEASE_ATTRIBUTES); + + for (String attributeRelease : entityDescriptorRepresentation.getAttributeRelease()) { + XSString xsString = (XSString) openSamlObjects.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + xsString.setValue(attributeRelease); + attribute.getAttributeValues().add(xsString); + } + } + return (List)(List)list; + } + + @Override + public List getAttributeListFromAttributeReleaseList(List attributeReleaseList) { + List attributeList = new ArrayList<>(); + + if (attributeReleaseList != null && attributeReleaseList.size() > 0) { + attributeList.add(attributeUtility.createAttributeWithStringValues(MDDCConstants.RELEASE_ATTRIBUTES, attributeReleaseList)); + } + + return (List)(List)attributeList; + } + + @Override + public List getAttributeListFromRelyingPartyOverridesRepresentation(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation) { + List list = new ArrayList<>(); + + if (relyingPartyOverridesRepresentation != null) { + if (relyingPartyOverridesRepresentation.isSignAssertion()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)); + } + if (relyingPartyOverridesRepresentation.isDontSignResponse()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)); + } + if (relyingPartyOverridesRepresentation.isTurnOffEncryption()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)); + } + if (relyingPartyOverridesRepresentation.isUseSha()) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")); + } + if (relyingPartyOverridesRepresentation.isIgnoreAuthenticationMethod()) { + // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate + // TODO: fix + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")); + } + if (relyingPartyOverridesRepresentation.isOmitNotBefore()) { + list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)); + } + if (relyingPartyOverridesRepresentation.getResponderId() != null && !"".equals(relyingPartyOverridesRepresentation.getResponderId())) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, relyingPartyOverridesRepresentation.getResponderId())); + } + if (relyingPartyOverridesRepresentation.getNameIdFormats() != null && relyingPartyOverridesRepresentation.getNameIdFormats().size() > 0) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, relyingPartyOverridesRepresentation.getNameIdFormats())); + } + if (relyingPartyOverridesRepresentation.getAuthenticationMethods() != null && relyingPartyOverridesRepresentation.getAuthenticationMethods().size() > 0) { + list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, relyingPartyOverridesRepresentation.getAuthenticationMethods())); + } + } + + return (List)(List)list; + } + + + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java new file mode 100644 index 000000000..a7bc130e4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImpl.java @@ -0,0 +1,67 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.List; + +/** + * Default implementation of {@link FilterService} + * + * @since 1.0 + * @author Bill Smith (wsmith@unicon.net) + */ +public class JPAFilterServiceImpl implements FilterService { + + private static final Logger LOGGER = LoggerFactory.getLogger(JPAFilterServiceImpl.class); + + @Autowired + EntityDescriptorService entityDescriptorService; + + @Autowired + EntityService entityService; + + @Autowired + FilterTargetService filterTargetService; + + @Override + public EntityAttributesFilter createFilterFromRepresentation(FilterRepresentation representation) { + //TODO? use OpenSamlObjects.buildDefaultInstanceOfType(EntityAttributesFilter.class)? + EntityAttributesFilter filter = new EntityAttributesFilter(); + + filter.setName(representation.getFilterName()); + filter.setFilterEnabled(representation.isFilterEnabled()); + + List attributeList = new ArrayList<>(); + attributeList.addAll(entityService.getAttributeListFromAttributeReleaseList(representation.getAttributeRelease())); + attributeList.addAll(entityService.getAttributeListFromRelyingPartyOverridesRepresentation(representation.getRelyingPartyOverrides())); + filter.setAttributes((List)(List)attributeList); // this makes me a sad panda. + + filter.setEntityAttributesFilterTarget(filterTargetService.createFilterTargetFromRepresentation(representation.getFilterTarget())); + + return filter; + } + + @Override + public FilterRepresentation createRepresentationFromFilter(EntityAttributesFilter entityAttributesFilter) { + FilterRepresentation representation = new FilterRepresentation(); + + representation.setId(entityAttributesFilter.getResourceId()); + representation.setFilterName(entityAttributesFilter.getName()); + representation.setFilterEnabled(entityAttributesFilter.isFilterEnabled()); + representation.setCreatedDate(entityAttributesFilter.getCreatedDate()); + representation.setModifiedDate(entityAttributesFilter.getModifiedDate()); + + representation.setAttributeRelease( + entityDescriptorService.getAttributeReleaseListFromAttributeList(entityAttributesFilter.getAttributes())); + representation.setRelyingPartyOverrides( + entityDescriptorService.getRelyingPartyOverridesRepresentationFromAttributeList(entityAttributesFilter.getAttributes())); + + representation.setFilterTarget(filterTargetService.createRepresentationFromFilterTarget(entityAttributesFilter.getEntityAttributesFilterTarget())); + return representation; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImpl.java new file mode 100644 index 000000000..a28d5a5ee --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImpl.java @@ -0,0 +1,30 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class JPAFilterTargetServiceImpl implements FilterTargetService { + @Override + public EntityAttributesFilterTarget createFilterTargetFromRepresentation(FilterTargetRepresentation representation) { + EntityAttributesFilterTarget filterTarget = new EntityAttributesFilterTarget(); + + filterTarget.setValue(representation.getValue()); + filterTarget.setEntityAttributesFilterTargetType( + EntityAttributesFilterTarget.EntityAttributesFilterTargetType.valueOf(representation.getType())); + + return filterTarget; + } + + @Override + public FilterTargetRepresentation createRepresentationFromFilterTarget(EntityAttributesFilterTarget entityAttributesFilterTarget) { + FilterTargetRepresentation representation = new FilterTargetRepresentation(); + + representation.setValue(entityAttributesFilterTarget.getValue()); + representation.setType(entityAttributesFilterTarget.getEntityAttributesFilterTargetType().name()); + + return representation; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.java new file mode 100644 index 000000000..5daed6f1c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.java @@ -0,0 +1,71 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import com.google.common.base.Predicate; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import net.shibboleth.utilities.java.support.resolver.ResolverException; +import org.opensaml.saml.common.profile.logic.EntityIdPredicate; +import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver; +import org.opensaml.saml.metadata.resolver.MetadataResolver; +import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilter; +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; +import org.opensaml.saml.saml2.core.Attribute; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.*; + +public class JPAMetadataResolverServiceImpl implements MetadataResolverService { + private static final Logger logger = LoggerFactory.getLogger(JPAMetadataResolverServiceImpl.class); + + @Autowired + private MetadataResolver metadataResolver; + + @Autowired + private MetadataResolverRepository metadataResolverRepository; + + // TODO: enhance + @Override + public void reloadFilters(String metadataResolverName) { + ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver)metadataResolver; + + MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().stream().filter(r -> r.getId().equals(metadataResolverName)).findFirst().get(); + edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByName(metadataResolverName); + + if (targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { + MetadataFilterChain metadataFilterChain = (MetadataFilterChain)targetMetadataResolver.getMetadataFilter(); + + List metadataFilters = new ArrayList<>(); + + for (edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { + if (metadataFilter instanceof EntityAttributesFilter) { + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter; + + org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter(); + Map, Collection> rules = new HashMap<>(); + if (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType() == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY) { + rules.put( + new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), + (List)(List)entityAttributesFilter.getAttributes() + ); + } + target.setRules(rules); + metadataFilters.add(target); + } + } + metadataFilterChain.setFilters(metadataFilters); + } + + if (metadataResolver instanceof RefreshableMetadataResolver) { + try { + ((RefreshableMetadataResolver)metadataResolver).refresh(); + } catch (ResolverException e) { + logger.warn("error refreshing metadataResolver " + metadataResolverName, e); + } + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java new file mode 100644 index 000000000..732693bd7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolverService.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +public interface MetadataResolverService { + public void reloadFilters(String metadataResolverName); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/AttributeUtility.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/AttributeUtility.java new file mode 100644 index 000000000..e3238b36a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/AttributeUtility.java @@ -0,0 +1,67 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeValue; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString; +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import org.opensaml.core.xml.schema.XSBooleanValue; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class AttributeUtility { + + @Autowired + private OpenSamlObjects openSamlObjects; + + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithBooleanValue(String name, String friendlyName, Boolean value) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + attribute.setName(name); + attribute.setFriendlyName(friendlyName); + attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); + + XSBoolean xsBoolean = (XSBoolean) openSamlObjects.getBuilderFactory().getBuilder(XSBoolean.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSBoolean.TYPE_NAME); + xsBoolean.setValue(XSBooleanValue.valueOf(value.toString())); + + attribute.getAttributeValues().add(xsBoolean); + return attribute; + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithStringValues(String name, List values) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder) openSamlObjects.getBuilderFactory() + .getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + attribute.setName(name); + //TODO: Do we need a friendlyName? + //TODO: Do we need a NameFormat? + + values.forEach(attributeString -> { + XSString xsString = (XSString) openSamlObjects.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + xsString.setValue(attributeString); + attribute.getAttributeValues().add(xsString); + }); + return attribute; + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithArbitraryValues(String name, String friendlyName, String... values) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); + attribute.setName(name); + attribute.setFriendlyName(friendlyName); + attribute.setNameFormat("urn:oasis:names:tc:SAML:2.0:attrname-format:uri"); + + for (String value : values) { + XSAny xsAny = (XSAny) openSamlObjects.getBuilderFactory().getBuilder(XSAny.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME); + xsAny.setTextContent(value); + attribute.getAttributeValues().add(xsAny); + } + + return attribute; + } + + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute createAttributeWithArbitraryValues(String name, String friendlyName, List values) { + return createAttributeWithArbitraryValues(name, friendlyName, values.toArray(new String[]{})); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/FilterTargetRepresentationDeserializer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/FilterTargetRepresentationDeserializer.java new file mode 100644 index 000000000..5d52d2fe5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/FilterTargetRepresentationDeserializer.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation; +import jdk.nashorn.internal.runtime.regexp.joni.ast.StringNode; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class FilterTargetRepresentationDeserializer extends StdDeserializer { + + public FilterTargetRepresentationDeserializer() { + this(null); + } + + public FilterTargetRepresentationDeserializer(Class vc) { + super(vc); + } + + @Override + public FilterTargetRepresentation deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + FilterTargetRepresentation representation = new FilterTargetRepresentation(); + + JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser); + String type = jsonNode.get("type").textValue(); + List values = new ArrayList<>(); + for (JsonNode valuesNode : jsonNode.get("value")) { + values.add(valuesNode.textValue()); + } + + representation.setType(type); + representation.setValue(values); + + return representation; + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy new file mode 100644 index 000000000..6ba9715a9 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy @@ -0,0 +1,69 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.reactive.server.WebTestClient +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("no-auth") +class EntitiesControllerIntegrationTests extends Specification { + + @Autowired + private WebTestClient webClient + + def "GET /api/entities returns the proper json"() { + given: + def expectedBody = ''' + { + "id":null, + "serviceProviderName":null, + "entityId":"http://test.scaldingspoon.org/test1", + "organization":null, + "contacts":null, + "mdui":null, + "serviceProviderSsoDescriptor": { + "protocolSupportEnum":"SAML 2", + "nameIdFormats":["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"] + }, + "logoutEndpoints":null, + "securityInfo":null, + "assertionConsumerServices":[ + {"locationUrl":"https://test.scaldingspoon.org/test1/acs","binding":"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST","makeDefault":false} + ], + "serviceEnabled":false, + "createdDate":null, + "modifiedDate":null, + "relyingPartyOverrides":{ + "signAssertion":false, + "dontSignResponse":false, + "turnOffEncryption":false, + "useSha":false, + "ignoreAuthenticationMethod":false, + "omitNotBefore":false, + "responderId":null, + "nameIdFormats":[], + "authenticationMethods":[] + }, + "attributeRelease":["givenName","employeeNumber"] + } + ''' + + when: + def result = this.webClient + .get() + .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") + .exchange() + + then: + result.expectStatus().isOk() + .expectBody().consumeWith( + { response -> new String(response.getResponseBody()) == expectedBody } + ) + //.expectedBody() // some other json comparison + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy new file mode 100644 index 000000000..89a43e4a8 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerTests.groovy @@ -0,0 +1,133 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl +import net.shibboleth.ext.spring.resource.ResourceHelper +import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver +import org.springframework.core.io.ClassPathResource +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Specification +import spock.lang.Subject + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class EntitiesControllerTests extends Specification { + def openSamlObjects = new OpenSamlObjects().with { + init() + it + } + + def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) + + def metadataResolver = new ResourceBackedMetadataResolver(resource).with { + it.id = 'test' + it.parserPool = openSamlObjects.parserPool + initialize() + it + } + + @Subject + def controller = new EntitiesController( + openSamlObjects: openSamlObjects, + entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)), + metadataResolver: metadataResolver + ) + + def mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + def 'GET /api/entities/test'() { + when: + def result = mockMvc.perform(get("/api/entities/test")) + + then: + result.andExpect(status().isNotFound()) + } + + def 'GET /api/entities/test XML'() { + when: + def result = mockMvc.perform(get("/api/entities/test").header('Accept', 'application/xml')) + + then: + result.andExpect(status().isNotFound()) + } + + def 'GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { + given: + def expectedBody = ''' + { + "id":null, + "serviceProviderName":null, + "entityId":"http://test.scaldingspoon.org/test1", + "organization":null, + "contacts":null, + "mdui":null, + "serviceProviderSsoDescriptor": { + "protocolSupportEnum":"SAML 2", + "nameIdFormats":["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"] + }, + "logoutEndpoints":null, + "securityInfo":null, + "assertionConsumerServices":[ + {"locationUrl":"https://test.scaldingspoon.org/test1/acs","binding":"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST","makeDefault":false} + ], + "serviceEnabled":false, + "createdDate":null, + "modifiedDate":null, + "relyingPartyOverrides":{ + "signAssertion":false, + "dontSignResponse":false, + "turnOffEncryption":false, + "useSha":false, + "ignoreAuthenticationMethod":false, + "omitNotBefore":false, + "responderId":null, + "nameIdFormats":[], + "authenticationMethods":[] + }, + "attributeRelease":["givenName","employeeNumber"] + } + ''' + when: + def result = mockMvc.perform(get('/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) + + then: + def x = content() + result.andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(content().json(expectedBody, false)) + } + + def 'GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML'() { + given: + def expectedBody = ''' + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + +''' + when: + def result = mockMvc.perform(get('/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1').header('Accept', 'application/xml')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType('application/xml;charset=ISO-8859-1')) + .andExpect(content().xml(expectedBody)) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index 91779a493..205e91e9b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -4,6 +4,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor 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.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import org.springframework.test.web.servlet.setup.MockMvcBuilders import spock.lang.Specification import spock.lang.Subject @@ -30,7 +31,7 @@ class EntityDescriptorControllerTests extends Specification { def controller = new EntityDescriptorController ( entityDescriptorRepository: entityDescriptorRepository, openSamlObjects: openSamlObjects, - entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects) + entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) ) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FilterControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FilterControllerTests.groovy new file mode 100644 index 000000000..f69883052 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FilterControllerTests.groovy @@ -0,0 +1,194 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.service.FilterService +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.result.MockMvcResultHandlers +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Specification + +import static org.hamcrest.CoreMatchers.containsString +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class FilterControllerTests extends Specification { + + @Autowired + AttributeUtility attributeUtility + + @Autowired + FilterService filterService + + TestObjectGenerator testObjectGenerator + RandomGenerator randomGenerator + ObjectMapper mapper + + def metadataResolverRepository = Mock(MetadataResolverRepository) + + def controller + + def mockMvc + + def mockFilterService = Mock(FilterService) + + def setup() { + randomGenerator = new RandomGenerator() + testObjectGenerator = new TestObjectGenerator(attributeUtility) + mapper = new ObjectMapper() + + controller = new FilterController ( + repository: metadataResolverRepository, + filterService: filterService, + metadataResolverService: new MetadataResolverService() { + @Override + void reloadFilters(String metadataResolverName) { + // we do nothing 'cause we're lazy + } + } + ) + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } + + def "FilterController.getAll gets all filters"() { + given: + def metadataResolver = new MetadataResolver() + metadataResolver.setMetadataFilters(testObjectGenerator.buildFilterList()) + List metadataResolverList = [metadataResolver] + 1 * metadataResolverRepository.findAll() >> metadataResolverList + + def expectedContent = [] + metadataResolver.getMetadataFilters().each { + expectedContent.add(filterService.createRepresentationFromFilter(it)) + } + def expectedHttpResponseStatus = status().isOk() + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform(get('/api/MetadataResolver/foo/Filters')) + + then: + result.andExpect(expectedHttpResponseStatus) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(mapper.writeValueAsString(expectedContent))) + } + + def "FilterController.getOne gets the desired filter"() { + given: + def metadataResolver = new MetadataResolver() + metadataResolver.setMetadataFilters(testObjectGenerator.buildFilterList()) + List metadataResolverList = [metadataResolver] + 1 * metadataResolverRepository.findAll() >> metadataResolverList + + def expectedFilter = filterService.createRepresentationFromFilter( + chooseRandomFilterFromList(metadataResolver.metadataFilters)) + def expectedFilterId = expectedFilter.id + def expectedHttpResponseStatus = status().isOk() + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform(get("/api/MetadataResolver/foo/Filter/$expectedFilterId")) + + then: + result.andExpect(expectedHttpResponseStatus) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(mapper.writeValueAsString(expectedFilter))) + .andDo(MockMvcResultHandlers.print()) + } + + def "FilterController.create creates the desired filter"() { + given: + controller.filterService = mockFilterService // so we can control ids + + def randomFilter = testObjectGenerator.buildEntityAttributesFilter() + def metadataResolver = new MetadataResolver() + metadataResolver.setResourceId(randomGenerator.randomId()) + metadataResolver.setMetadataFilters(testObjectGenerator.buildFilterList()) + def metadataResolverWithFilter = new MetadataResolver() + metadataResolverWithFilter.resourceId = metadataResolver.resourceId + metadataResolverWithFilter.metadataFilters = metadataResolver.metadataFilters.collect() + metadataResolverWithFilter.getMetadataFilters().add(randomFilter) + + 1 * metadataResolverRepository.findAll() >> [metadataResolver] + 1 * metadataResolverRepository.save(_) >> metadataResolverWithFilter + 1 * mockFilterService.createFilterFromRepresentation(_) >> randomFilter // this is where we want to control the id + 1 * mockFilterService.createRepresentationFromFilter(randomFilter) >> filterService.createRepresentationFromFilter(randomFilter) + + def expectedMetadataResolverUUID = metadataResolver.getResourceId() + def expectedFilterUUID = randomFilter.getResourceId() + def expectedResponseHeader = 'Location' + def expectedResponseHeaderValue = "/api/MetadataResolver/$expectedMetadataResolverUUID/Filter/$expectedFilterUUID" + def expectedJsonBody = mapper.writeValueAsString(filterService.createRepresentationFromFilter(randomFilter)) + def postedJsonBody = expectedJsonBody - ~/"id":.*?,/ // remove the "id:," + + when: + def result = mockMvc.perform( + post('/api/MetadataResolver/foo/Filter') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().json(expectedJsonBody, true)) + .andExpect(header().string(expectedResponseHeader, containsString(expectedResponseHeaderValue))) + } + + def "FilterController.update updates the target filter as desired"() { + given: + def randomFilter = testObjectGenerator.buildEntityAttributesFilter() + def updatedFilter = testObjectGenerator.buildEntityAttributesFilter() + updatedFilter.resourceId = randomFilter.resourceId + def postedJsonBody = mapper.writeValueAsString( + filterService.createRepresentationFromFilter(updatedFilter)) + + def originalMetadataResolver = new MetadataResolver() + originalMetadataResolver.setResourceId(randomGenerator.randomId()) + originalMetadataResolver.setMetadataFilters(testObjectGenerator.buildFilterList()) + def updatedMetadataResolver = new MetadataResolver() + updatedMetadataResolver.setResourceId(originalMetadataResolver.getResourceId()) + updatedMetadataResolver.setMetadataFilters(originalMetadataResolver.getMetadataFilters().collect()) + originalMetadataResolver.getMetadataFilters().add(randomFilter) + updatedMetadataResolver.getMetadataFilters().add(updatedFilter) + + 1 * metadataResolverRepository.findAll() >> [originalMetadataResolver] + 1 * metadataResolverRepository.save(_) >> updatedMetadataResolver + + def filterUUID = randomFilter.getResourceId() + + when: + def result = mockMvc.perform( + put("/api/MetadataResolver/foo/Filter/$filterUUID") + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isOk()) + .andExpect(content().json(postedJsonBody, true)) + } + + EntityAttributesFilter chooseRandomFilterFromList(List filters) { + filters.get(randomGenerator.randomInt(0, filters.size() - 1)) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTargetTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTargetTests.groovy new file mode 100644 index 000000000..520193f4b --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTargetTests.groovy @@ -0,0 +1,37 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.ObjectMapper +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +class EntityAttributesFilterTargetTests extends Specification { + + def "when setValue is passed a list, it sets the list"() { + given: + def filterTarget = new EntityAttributesFilterTarget() + def stringsList = ["one", "two", "three"] + + when: + filterTarget.setValue(stringsList) + + then: + filterTarget.value == stringsList + } + + def "when setValue is passed a single string, it creates a new list with that string"() { + given: + def filterTarget = new EntityAttributesFilterTarget() + def someString = "someString" + def expectedList = [someString] + + when: + filterTarget.setValue(someString) + + then: + filterTarget.value.size() == 1 + filterTarget.value == expectedList + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTest.groovy new file mode 100644 index 000000000..713471f48 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTest.groovy @@ -0,0 +1,52 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * A highly unnecessary test so that I can check to make sure that persistence is correct for the model + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class MetadataResolverRepositoryTest extends Specification { + @Autowired + MetadataResolverRepository metadataResolverRepository + + def "test persisting a metadata resolver"() { + when: + def mdr = new MetadataResolver().with { + it.name = "testme" + it.metadataFilters.add(new EntityAttributesFilter().with { + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + it.setValue(["hola"]) + return it + } + return it + }) + return it + } + metadataResolverRepository.save(mdr) + + then: + metadataResolverRepository.findAll().size() > 0 + MetadataResolver item = metadataResolverRepository.findByName("testme") + item.name == "testme" + item.metadataFilters.size() == 1 + item.metadataFilters.get(0).entityAttributesFilterTarget.entityAttributesFilterTargetType == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + item.metadataFilters.get(0).entityAttributesFilterTarget.value.size() == 1 + item.metadataFilters.get(0).entityAttributesFilterTarget.value.get(0) == "hola" + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index 04a094ab6..5e4a2d0a0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -3,8 +3,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.* import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects -import org.assertj.core.api.Assertions -import org.opensaml.security.credential.UsageType import org.springframework.boot.test.json.JacksonTester import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input @@ -18,7 +16,7 @@ class JPAEntityDescriptorServiceImplTests extends Specification { it } - def service = new JPAEntityDescriptorServiceImpl(openSamlObjects) + def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) JacksonTester jacksonTester diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy new file mode 100644 index 000000000..d09e04d1f --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy @@ -0,0 +1,100 @@ +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.MetadataResolverConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import org.apache.commons.lang.StringUtils +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.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class JPAFilterServiceImplTests extends Specification { + + RandomGenerator randomGenerator + TestObjectGenerator testObjectGenerator + + @Autowired + JPAFilterServiceImpl service + + @Autowired + AttributeUtility attributeUtility + + def setup() { + randomGenerator = new RandomGenerator() + testObjectGenerator = new TestObjectGenerator(attributeUtility) + } + + def "createFilterFromRepresentation properly creates a filter from a representation"() { + given: + def representation = testObjectGenerator.buildFilterRepresentation() + + when: + def result = service.createFilterFromRepresentation(representation) + + then: + result.name == representation.filterName + // Note: We don't test result.resourceId == representation.id because representations only have ids when built from filters. + result.filterEnabled == representation.filterEnabled + + //TODO? Should we test that the actual attributes are what we expect them to be? Would make for a much more + //complicated test. Testing size is fairly useful, but it forces us to assume that the attributes are what they + //should be. Maybe testing that the attributes are what they should be should be done in a unit test for the + //actual method that builds the attributes list? + result.getAttributes().size() == determineCountOfAttributesFromRelyingPartyOverrides(representation) + + result.entityAttributesFilterTarget.value == representation.filterTarget.value + result.entityAttributesFilterTarget.entityAttributesFilterTargetType.toString() == representation.filterTarget.type + } + + def "createRepresentationFromFilter properly creates a representation from a filter"() { + given: + def filter = testObjectGenerator.buildEntityAttributesFilter() + + when: + def result = service.createRepresentationFromFilter(filter) + + then: + result.id == filter.resourceId + result.filterName == filter.name + result.filterEnabled == filter.filterEnabled + + //TODO? See note above, same question. + determineCountOfAttributesFromRelyingPartyOverrides(result) == filter.getAttributes().size() + + result.filterTarget.type == filter.entityAttributesFilterTarget.entityAttributesFilterTargetType.toString() + result.filterTarget.value == filter.entityAttributesFilterTarget.value + } + + int determineCountOfAttributesFromRelyingPartyOverrides(FilterRepresentation representation) { + int count = 0 + + count += representation.getAttributeRelease().size() + RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation = representation.getRelyingPartyOverrides() + count += relyingPartyOverridesRepresentation.authenticationMethods.size() != 0 ? 1 : 0 + count += relyingPartyOverridesRepresentation.dontSignResponse ? 1 : 0 + count += relyingPartyOverridesRepresentation.ignoreAuthenticationMethod ? 1 : 0 + count += relyingPartyOverridesRepresentation.nameIdFormats.size() != 0 ? 1 : 0 + count += relyingPartyOverridesRepresentation.omitNotBefore ? 1 : 0 + count += relyingPartyOverridesRepresentation.signAssertion ? 1 : 0 + count += relyingPartyOverridesRepresentation.turnOffEncryption ? 1 : 0 + count += relyingPartyOverridesRepresentation.useSha ? 1 : 0 + count += StringUtils.isNotBlank(relyingPartyOverridesRepresentation.responderId) ? 1 : 0 + + return count + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy new file mode 100644 index 000000000..80db7c8e5 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterTargetServiceImplTests.groovy @@ -0,0 +1,46 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +class JPAFilterTargetServiceImplTests extends Specification { + + RandomGenerator randomGenerator + TestObjectGenerator testObjectGenerator + + JPAFilterTargetServiceImpl service + + def setup() { + randomGenerator = new RandomGenerator() + testObjectGenerator = new TestObjectGenerator() + service = new JPAFilterTargetServiceImpl() + } + + def "createFilterTargetFromRepresentation properly creates a filter target"() { + given: + def representation = testObjectGenerator.buildFilterTargetRepresentation() + + when: + def results = service.createFilterTargetFromRepresentation(representation) + + then: + results.value == representation.value + results.entityAttributesFilterTargetType.toString() == representation.getType() + } + + def "createRepresentationFromFilterTarget properly creates a representation from a filter target"() { + given: + def filterTarget = testObjectGenerator.buildEntityAttributesFilterTarget() + + when: + def results = service.createRepresentationFromFilterTarget(filterTarget) + + then: + results.value == filterTarget.value + results.type == filterTarget.entityAttributesFilterTargetType.toString() + } +} 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 new file mode 100644 index 000000000..efcb9e040 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -0,0 +1,131 @@ +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.MetadataResolverConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import net.shibboleth.ext.spring.resource.ResourceHelper +import net.shibboleth.utilities.java.support.resolver.CriteriaSet +import org.joda.time.DateTime +import org.opensaml.core.criterion.EntityIdCriterion +import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver +import org.opensaml.saml.metadata.resolver.MetadataResolver +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain +import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver +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.core.io.ClassPathResource +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.xmlunit.builder.DiffBuilder +import org.xmlunit.builder.Input +import spock.lang.Specification + +@SpringBootTest +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class JPAMetadataResolverServiceImplTests extends Specification { + @Autowired + MetadataResolverRepository metadataResolverRepository + + @Autowired + MetadataResolverService metadataResolverService + + @Autowired + MetadataResolver metadataResolver + + @Autowired + EntityService entityService + + @Autowired + OpenSamlObjects openSamlObjects + + def 'test adding a filter'() { + given: + def expectedXML = ''' + + + + + internal + + + givenName + employeeNumber + + + testme + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + +''' + when: + def mdr = new edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver().with { + it.name = "testme" + it.metadataFilters.add(new EntityAttributesFilter().with { + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + it.setValue(['http://test.scaldingspoon.org/test1']) + return it + } + it.attributes = entityService.getAttributeListFromAttributeReleaseList(['testme']) + return it + }) + return it + } + metadataResolverRepository.save(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) + def diff = DiffBuilder.compare(Input.fromString(expectedXML)).withTest(Input.fromString(resultString)).ignoreComments().ignoreWhitespace().build() + !diff.hasDifferences() + } + + @TestConfiguration + static class Config { + @Autowired + OpenSamlObjects openSamlObjects + + @Bean + MetadataResolver metadataResolver() { + def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) + def aggregate = new ResourceBackedMetadataResolver(resource){ + @Override + DateTime getLastRefresh() { + return null + } + } + + aggregate.with { + it.metadataFilter = new MetadataFilterChain() + it.id = 'testme' + it.parserPool = openSamlObjects.parserPool + it.initialize() + it + } + + return new ChainingMetadataResolver().with { + it.id = 'chain' + it.resolvers = [aggregate] + it.initialize() + it + } + } + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/RandomGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/RandomGenerator.groovy new file mode 100644 index 000000000..99e365705 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/RandomGenerator.groovy @@ -0,0 +1,69 @@ +package edu.internet2.tier.shibboleth.admin.ui.util + +import org.apache.commons.lang3.RandomStringUtils + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +class RandomGenerator { + + def rand = new Random() + + String randomId() { + return UUID.randomUUID().toString() + } + + String randomString() { + return randomId() //Good enough for now + } + + String randomString(int min, int max) { + int length = randomRangeInt(min, max) + return RandomStringUtils.randomAlphanumeric(length) + } + + String randomString(int n) { + return RandomStringUtils.randomAlphanumeric(n) + } + + int randomRangeInt(int min, int max){ + def val = randomInt(min, max) + return (val > max) ? max : val + } + + int randomInt(int min, int max) { + if (min == max) { + return min + } else { + return min + rand.nextInt(max - min) + } + } + + Date randomDate() { + return randomDate(randomBoolean()) + } + + Date randomDateBeforeNow() { + return randomDate(true) + } + + //return a date either from before now or after now + Date randomDate(boolean beforeOrAfter) { + def now = System.currentTimeMillis() + def incr = Math.abs(rand.nextLong()) % 10000000 * (beforeOrAfter ? -1 : 1) + def time = now + incr + return new Date(time) + } + + boolean randomBoolean() { + return rand.nextBoolean() + } + + List randomStringList() { + def stringList = new ArrayList() + [0..randomInt(1, 10)].each { + stringList.add(randomString(10)) + } + return stringList + } +} 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 new file mode 100644 index 000000000..aaf9c23d2 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -0,0 +1,133 @@ +package edu.internet2.tier.shibboleth.admin.ui.util + +import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import edu.internet2.tier.shibboleth.admin.util.MDDCConstants + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +class TestObjectGenerator { + + AttributeUtility attributeUtility + + RandomGenerator generator = new RandomGenerator() + + TestObjectGenerator(AttributeUtility attributeUtility) { + this.attributeUtility = attributeUtility + } + + List buildFilterList() { + List filterList = new ArrayList<>() + (1..generator.randomInt(4, 10)).each { + filterList.add(buildEntityAttributesFilter()) + } + return filterList + } + + EntityAttributesFilter buildEntityAttributesFilter() { + EntityAttributesFilter filter = new EntityAttributesFilter() + + filter.setName(generator.randomString(10)) + filter.setFilterEnabled(generator.randomBoolean()) + filter.setResourceId(generator.randomId()) + filter.setEntityAttributesFilterTarget(buildEntityAttributesFilterTarget()) + filter.setAttributes(buildAttributesList()) + + return filter + } + + List buildAttributesList() { + List attributes = new ArrayList<>() + + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")) + } + if (generator.randomBoolean()) { + // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate + // TODO: fix + attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, generator.randomId())) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, generator.randomStringList())) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, generator.randomStringList())) + } + if (generator.randomBoolean()) { + attributes.add(attributeUtility.createAttributeWithStringValues(MDDCConstants.RELEASE_ATTRIBUTES, generator.randomStringList())) + } + + return attributes + } + + FilterRepresentation buildFilterRepresentation() { + FilterRepresentation representation = new FilterRepresentation() + + representation.setFilterName(generator.randomString(10)) + representation.setAttributeRelease(generator.randomStringList()) + representation.setFilterEnabled(generator.randomBoolean()) + representation.setFilterTarget(buildFilterTargetRepresentation()) + representation.setRelyingPartyOverrides(buildRelyingPartyOverridesRepresentation()) + + return representation + } + + RelyingPartyOverridesRepresentation buildRelyingPartyOverridesRepresentation() { + RelyingPartyOverridesRepresentation representation = new RelyingPartyOverridesRepresentation() + + representation.setAuthenticationMethods(generator.randomStringList()) + representation.setDontSignResponse(generator.randomBoolean()) + representation.setIgnoreAuthenticationMethod(generator.randomBoolean()) + representation.setNameIdFormats(generator.randomStringList()) + representation.setOmitNotBefore(generator.randomBoolean()) + representation.setSignAssertion(generator.randomBoolean()) + representation.setTurnOffEncryption(generator.randomBoolean()) + representation.setUseSha(generator.randomBoolean()) + representation.setResponderId(generator.randomId()) + + return representation + } + + EntityAttributesFilterTarget buildEntityAttributesFilterTarget() { + EntityAttributesFilterTarget entityAttributesFilterTarget = new EntityAttributesFilterTarget() + + entityAttributesFilterTarget.setValue(generator.randomStringList()) + entityAttributesFilterTarget.setEntityAttributesFilterTargetType(randomFilterTargetType()) + + return entityAttributesFilterTarget + } + + FilterTargetRepresentation buildFilterTargetRepresentation() { + FilterTargetRepresentation representation = new FilterTargetRepresentation() + + representation.setValue(generator.randomStringList()) + representation.setType(randomFilterTargetType().toString()) + + return representation + } + + EntityAttributesFilterTarget.EntityAttributesFilterTargetType randomFilterTargetType() { + EntityAttributesFilterTarget.EntityAttributesFilterTargetType.values()[generator.randomInt(0, 2)] + } +} diff --git a/backend/src/test/resources/json/SHIBUI-414.json b/backend/src/test/resources/json/SHIBUI-414.json new file mode 100644 index 000000000..9c0b33d22 --- /dev/null +++ b/backend/src/test/resources/json/SHIBUI-414.json @@ -0,0 +1,14 @@ +{ + "id": "", + "filterName": "test", + "filterEnabled": false, + "relyingPartyOverrides": { + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [], + "filterTarget": { + "type": "entity", + "value": "https://test/sp" + } +} \ No newline at end of file diff --git a/backend/src/test/resources/metadata/aggregate.xml b/backend/src/test/resources/metadata/aggregate.xml new file mode 100644 index 000000000..61195b1be --- /dev/null +++ b/backend/src/test/resources/metadata/aggregate.xml @@ -0,0 +1,27 @@ + + + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + + \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 628fa98d6..5ff0025d3 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -211,6 +211,11 @@ "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-5.1.0.tgz", "integrity": "sha1-zvhFdrLQMz8ZGIrt/hVv0wG/9wo=" }, + "@ngrx/entity": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/entity/-/entity-5.2.0.tgz", + "integrity": "sha1-aLkLMVm7RuxbLQ3j+gv06A6Uly0=" + }, "@ngrx/router-store": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-5.0.1.tgz", diff --git a/ui/package.json b/ui/package.json index 56f78f800..7f0f24bcd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -28,6 +28,7 @@ "@angular/router": "5.2.7", "@ng-bootstrap/ng-bootstrap": "^1.0.0", "@ngrx/effects": "^5.1.0", + "@ngrx/entity": "^5.2.0", "@ngrx/router-store": "^5.0.1", "@ngrx/store": "^5.1.0", "@reactivex/rxjs": "^5.5.6", diff --git a/ui/src/app/app.module.ts b/ui/src/app/app.module.ts index 6ca4b3c4c..04f1e3fff 100644 --- a/ui/src/app/app.module.ts +++ b/ui/src/app/app.module.ts @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { StoreRouterConnectingModule, RouterStateSerializer } from '@ngrx/router-store'; -import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; @@ -44,7 +44,8 @@ import { environment } from '../environments/environment'; NgbModalModule.forRoot(), NgbPopoverModule.forRoot(), NgbPaginationModule.forRoot(), - NotificationModule + NotificationModule, + HttpClientModule ], providers: [ NavigatorService, diff --git a/ui/src/app/core/service/cache.interceptor.ts b/ui/src/app/core/service/cache.interceptor.ts index 03122055c..d9e38ec1c 100644 --- a/ui/src/app/core/service/cache.interceptor.ts +++ b/ui/src/app/core/service/cache.interceptor.ts @@ -8,7 +8,7 @@ class HttpCache { this.store = {}; } private generateKey(request: HttpRequest): string { - return `${request.method}.${request.urlWithParams}`; + return `${request.method}.${request.urlWithParams}.${request.responseType}`; } get(req: HttpRequest): HttpResponse | null { return this.store[this.generateKey(req)]; diff --git a/ui/src/app/dashboard/action/dashboard.action.ts b/ui/src/app/dashboard/action/dashboard.action.ts index 7eca8a597..cf95aa494 100644 --- a/ui/src/app/dashboard/action/dashboard.action.ts +++ b/ui/src/app/dashboard/action/dashboard.action.ts @@ -1,8 +1,6 @@ import { Action } from '@ngrx/store'; -import { MetadataEntity } from '../../domain/domain.type'; export const TOGGLE_ENTITY_DISPLAY = '[Dashboard] Display Entity'; -export const PREVIEW_ENTITY = '[Dashboard] Preview Entity'; export class ToggleEntityDisplay implements Action { readonly type = TOGGLE_ENTITY_DISPLAY; @@ -10,12 +8,5 @@ export class ToggleEntityDisplay implements Action { constructor(public payload: string) { } } -export class PreviewEntity implements Action { - readonly type = PREVIEW_ENTITY; - - constructor(public payload: MetadataEntity) { } -} - export type Actions = - | ToggleEntityDisplay - | PreviewEntity; + | ToggleEntityDisplay; diff --git a/ui/src/app/dashboard/container/dashboard.component.ts b/ui/src/app/dashboard/container/dashboard.component.ts index 62df819fb..8ac942f96 100644 --- a/ui/src/app/dashboard/container/dashboard.component.ts +++ b/ui/src/app/dashboard/container/dashboard.component.ts @@ -10,7 +10,8 @@ import * as searchActions from '../action/search.action'; import * as providerActions from '../../domain/action/provider-collection.action'; import * as draftActions from '../../domain/action/draft-collection.action'; import * as fromDashboard from '../reducer'; -import { ToggleEntityDisplay, PreviewEntity } from '../action/dashboard.action'; +import { ToggleEntityDisplay } from '../action/dashboard.action'; +import { PreviewEntity } from '../../domain/action/entity.action'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DeleteDialogComponent } from '../component/delete-dialog.component'; @@ -91,9 +92,7 @@ export class DashboardComponent implements OnInit { } openPreviewDialog(entity: MetadataEntity): void { - if (entity.type === DomainTypes.provider) { - this.store.dispatch(new PreviewEntity(entity)); - } + this.store.dispatch(new PreviewEntity(entity)); } deleteProvider(entity: MetadataProvider): void { diff --git a/ui/src/app/dashboard/dashboard.module.ts b/ui/src/app/dashboard/dashboard.module.ts index d4e457116..06efbc2b0 100644 --- a/ui/src/app/dashboard/dashboard.module.ts +++ b/ui/src/app/dashboard/dashboard.module.ts @@ -1,4 +1,5 @@ import { NgModule } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; import { RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; @@ -10,11 +11,11 @@ import { DashboardComponent } from './container/dashboard.component'; import { EntityItemComponent } from './component/entity-item.component'; import { ProviderSearchComponent } from './component/provider-search.component'; import { reducers } from './reducer'; -import { DashboardEffects } from './effect/dashboard.effect'; import { SearchEffects } from './effect/search.effects'; import { DeleteDialogComponent } from './component/delete-dialog.component'; import { NgbModalModule } from '@ng-bootstrap/ng-bootstrap/modal/modal.module'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { PreviewDialogModule } from '../shared/preview/preview-dialog.module'; @NgModule({ declarations: [ @@ -31,12 +32,14 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; { path: '', component: DashboardComponent } ]), StoreModule.forFeature('dashboard', reducers), - EffectsModule.forFeature([DashboardEffects, SearchEffects]), + EffectsModule.forFeature([SearchEffects]), CommonModule, ReactiveFormsModule, NgbPaginationModule, NgbModalModule, - NgbDropdownModule + NgbDropdownModule, + PreviewDialogModule, + HttpClientModule ], providers: [] }) diff --git a/ui/src/app/dashboard/effect/dashboard.effect.ts b/ui/src/app/dashboard/effect/dashboard.effect.ts deleted file mode 100644 index 5ea39d38b..000000000 --- a/ui/src/app/dashboard/effect/dashboard.effect.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Effect, Actions } from '@ngrx/effects'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; - -import * as dashboardActions from '../action/dashboard.action'; -import { PreviewProviderDialogComponent } from '../../metadata-provider/component/preview-provider-dialog.component'; - -@Injectable() -export class DashboardEffects { - - @Effect({ dispatch: false }) - previewProviderXml$ = this.actions$ - .ofType(dashboardActions.PREVIEW_ENTITY) - .map(action => action.payload) - .switchMap(provider => { - let modal = this.modalService.open(PreviewProviderDialogComponent, { - size: 'lg', - windowClass: 'modal-xl' - }); - modal.componentInstance.provider = provider; - return modal.result.then( - result => result, - err => err - ); - }); - - constructor( - private actions$: Actions, - private modalService: NgbModal - ) { } -} /* istanbul ignore next */ diff --git a/ui/src/app/domain/action/draft-collection.action.ts b/ui/src/app/domain/action/draft-collection.action.ts index ede97cd2c..39a631b2c 100644 --- a/ui/src/app/domain/action/draft-collection.action.ts +++ b/ui/src/app/domain/action/draft-collection.action.ts @@ -1,108 +1,110 @@ import { Action } from '@ngrx/store'; +import { Update } from '@ngrx/entity/entity'; import { MetadataProvider } from '../model/metadata-provider'; -export const FIND = '[Metadata Draft] Find'; -export const SELECT = '[Metadata Draft] Select'; - -export const UPDATE_DRAFT_REQUEST = '[Metadata Draft] Update Request'; -export const UPDATE_DRAFT_SUCCESS = '[Metadata Draft] Update Success'; -export const UPDATE_DRAFT_FAIL = '[Metadata Draft] Update Fail'; +export enum DraftCollectionActionTypes { + FIND = '[Metadata Draft] Find', + SELECT = '[Metadata Draft] Select', + UPDATE_DRAFT_REQUEST = '[Metadata Draft] Update Request', + UPDATE_DRAFT_SUCCESS = '[Metadata Draft] Update Success', + UPDATE_DRAFT_FAIL = '[Metadata Draft] Update Fail', + LOAD_DRAFT_REQUEST = '[Metadata Draft Collection] Draft REQUEST', + LOAD_DRAFT_SUCCESS = '[Metadata Draft Collection] Draft SUCCESS', + LOAD_DRAFT_ERROR = '[Metadata Draft Collection] Draft ERROR', + ADD_DRAFT = '[Metadata Draft Collection] Add Draft', + ADD_DRAFT_SUCCESS = '[Metadata Draft Collection] Add Draft Success', + ADD_DRAFT_FAIL = '[Metadata Draft Collection] Add Draft Fail', + REMOVE_DRAFT = '[Metadata Draft Collection] Remove Draft', + REMOVE_DRAFT_SUCCESS = '[Metadata Draft Collection] Remove Draft Success', + REMOVE_DRAFT_FAIL = '[Metadata Draft Collection] Remove Draft Fail' +} -export const LOAD_DRAFT_REQUEST = '[Metadata Draft Collection] Draft REQUEST'; -export const LOAD_DRAFT_SUCCESS = '[Metadata Draft Collection] Draft SUCCESS'; -export const LOAD_DRAFT_ERROR = '[Metadata Draft Collection] Draft ERROR'; -export const ADD_DRAFT = '[Metadata Draft Collection] Add Draft'; -export const ADD_DRAFT_SUCCESS = '[Metadata Draft Collection] Add Draft Success'; -export const ADD_DRAFT_FAIL = '[Metadata Draft Collection] Add Draft Fail'; -export const REMOVE_DRAFT = '[Metadata Draft Collection] Remove Draft'; -export const REMOVE_DRAFT_SUCCESS = '[Metadata Draft Collection] Remove Draft Success'; -export const REMOVE_DRAFT_FAIL = '[Metadata Draft Collection] Remove Draft Fail'; export class FindDraft implements Action { - readonly type = FIND; + readonly type = DraftCollectionActionTypes.FIND; constructor(public payload: string) { } } export class SelectDraft implements Action { - readonly type = SELECT; + readonly type = DraftCollectionActionTypes.SELECT; constructor(public payload: string) { } } export class UpdateDraftRequest implements Action { - readonly type = UPDATE_DRAFT_REQUEST; + readonly type = DraftCollectionActionTypes.UPDATE_DRAFT_REQUEST; constructor(public payload: MetadataProvider) { } } export class UpdateDraftSuccess implements Action { - readonly type = UPDATE_DRAFT_SUCCESS; + readonly type = DraftCollectionActionTypes.UPDATE_DRAFT_SUCCESS; - constructor(public payload: MetadataProvider) { } + constructor(public payload: Update) { } } export class UpdateDraftFail implements Action { - readonly type = UPDATE_DRAFT_FAIL; + readonly type = DraftCollectionActionTypes.UPDATE_DRAFT_FAIL; constructor(public payload: MetadataProvider) { } } export class AddDraftRequest implements Action { - readonly type = ADD_DRAFT; + readonly type = DraftCollectionActionTypes.ADD_DRAFT; constructor(public payload: MetadataProvider) { } } export class AddDraftSuccess implements Action { - readonly type = ADD_DRAFT_SUCCESS; + readonly type = DraftCollectionActionTypes.ADD_DRAFT_SUCCESS; constructor(public payload: MetadataProvider) { } } export class AddDraftFail implements Action { - readonly type = ADD_DRAFT_FAIL; + readonly type = DraftCollectionActionTypes.ADD_DRAFT_FAIL; constructor(public payload: any) { } } export class RemoveDraftRequest implements Action { - readonly type = REMOVE_DRAFT; + readonly type = DraftCollectionActionTypes.REMOVE_DRAFT; constructor(public payload: MetadataProvider) { } } export class RemoveDraftSuccess implements Action { - readonly type = REMOVE_DRAFT_SUCCESS; + readonly type = DraftCollectionActionTypes.REMOVE_DRAFT_SUCCESS; constructor(public payload: MetadataProvider) { } } export class RemoveDraftFail implements Action { - readonly type = REMOVE_DRAFT_FAIL; + readonly type = DraftCollectionActionTypes.REMOVE_DRAFT_FAIL; constructor(public payload: MetadataProvider) { } } export class LoadDraftRequest implements Action { - readonly type = LOAD_DRAFT_REQUEST; + readonly type = DraftCollectionActionTypes.LOAD_DRAFT_REQUEST; constructor() { } } export class LoadDraftSuccess implements Action { - readonly type = LOAD_DRAFT_SUCCESS; + readonly type = DraftCollectionActionTypes.LOAD_DRAFT_SUCCESS; constructor(public payload: MetadataProvider[]) { } } export class LoadDraftError implements Action { - readonly type = LOAD_DRAFT_ERROR; + readonly type = DraftCollectionActionTypes.LOAD_DRAFT_ERROR; constructor(public payload: any) { } } -export type Actions = +export type DraftCollectionActionsUnion = | LoadDraftRequest | LoadDraftSuccess | LoadDraftError diff --git a/ui/src/app/domain/action/entity.action.ts b/ui/src/app/domain/action/entity.action.ts new file mode 100644 index 000000000..61f3ad3e5 --- /dev/null +++ b/ui/src/app/domain/action/entity.action.ts @@ -0,0 +1,13 @@ +import { Action } from '@ngrx/store'; +import { MetadataEntity } from '../../domain/domain.type'; + +export const PREVIEW_ENTITY = '[Domain] Preview Entity'; + +export class PreviewEntity implements Action { + readonly type = PREVIEW_ENTITY; + + constructor(public payload: MetadataEntity) { } +} + +export type Actions = + | PreviewEntity; diff --git a/ui/src/app/domain/action/filter-collection.action.ts b/ui/src/app/domain/action/filter-collection.action.ts index 87a32f23e..621333897 100644 --- a/ui/src/app/domain/action/filter-collection.action.ts +++ b/ui/src/app/domain/action/filter-collection.action.ts @@ -1,122 +1,126 @@ import { Action } from '@ngrx/store'; import { MetadataFilter } from '../../domain/model/metadata-filter'; +import { Update } from '@ngrx/entity'; + +export enum FilterCollectionActionTypes { + FIND = '[Metadata Filter] Find', + SELECT = '[Metadata Filter] Select', + SELECT_FILTER_SUCCESS = '[Metadata Filter] Select Success', + SELECT_FILTER_FAIL = '[Metadata Filter] Select Fail', + + UPDATE_FILTER_REQUEST = '[Metadata Filter] Update Request', + UPDATE_FILTER_SUCCESS = '[Metadata Filter] Update Success', + UPDATE_FILTER_FAIL = '[Metadata Filter] Update Fail', + + LOAD_FILTER_REQUEST = '[Metadata Filter Collection] Filter REQUEST', + LOAD_FILTER_SUCCESS = '[Metadata Filter Collection] Filter SUCCESS', + LOAD_FILTER_ERROR = '[Metadata Filter Collection] Filter ERROR', + ADD_FILTER = '[Metadata Filter Collection] Add Filter', + ADD_FILTER_SUCCESS = '[Metadata Filter Collection] Add Filter Success', + ADD_FILTER_FAIL = '[Metadata Filter Collection] Add Filter Fail', + REMOVE_FILTER = '[Metadata Filter Collection] Remove Filter', + REMOVE_FILTER_SUCCESS = '[Metadata Filter Collection] Remove Filter Success', + REMOVE_FILTER_FAIL = '[Metadata Filter Collection] Remove Filter Fail' +} -export const FIND = '[Metadata Filter] Find'; -export const SELECT = '[Metadata Filter] Select'; -export const SELECT_FILTER_SUCCESS = '[Metadata Filter] Select Success'; -export const SELECT_FILTER_FAIL = '[Metadata Filter] Select Fail'; - -export const UPDATE_FILTER_REQUEST = '[Metadata Filter] Update Request'; -export const UPDATE_FILTER_SUCCESS = '[Metadata Filter] Update Success'; -export const UPDATE_FILTER_FAIL = '[Metadata Filter] Update Fail'; - -export const LOAD_FILTER_REQUEST = '[Metadata Filter Collection] Filter REQUEST'; -export const LOAD_FILTER_SUCCESS = '[Metadata Filter Collection] Filter SUCCESS'; -export const LOAD_FILTER_ERROR = '[Metadata Filter Collection] Filter ERROR'; -export const ADD_FILTER = '[Metadata Filter Collection] Add Filter'; -export const ADD_FILTER_SUCCESS = '[Metadata Filter Collection] Add Filter Success'; -export const ADD_FILTER_FAIL = '[Metadata Filter Collection] Add Filter Fail'; -export const REMOVE_FILTER = '[Metadata Filter Collection] Remove Filter'; -export const REMOVE_FILTER_SUCCESS = '[Metadata Filter Collection] Remove Filter Success'; -export const REMOVE_FILTER_FAIL = '[Metadata Filter Collection] Remove Filter Fail'; export class FindFilter implements Action { - readonly type = FIND; + readonly type = FilterCollectionActionTypes.FIND; constructor(public payload: string) { } } export class SelectFilter implements Action { - readonly type = SELECT; + readonly type = FilterCollectionActionTypes.SELECT; constructor(public payload: string) { } } export class SelectFilterSuccess implements Action { - readonly type = SELECT_FILTER_SUCCESS; + readonly type = FilterCollectionActionTypes.SELECT_FILTER_SUCCESS; constructor(public payload: MetadataFilter) { } } export class SelectFilterFail implements Action { - readonly type = SELECT_FILTER_FAIL; + readonly type = FilterCollectionActionTypes.SELECT_FILTER_FAIL; constructor(public payload: Error) { } } export class LoadFilterRequest implements Action { - readonly type = LOAD_FILTER_REQUEST; + readonly type = FilterCollectionActionTypes.LOAD_FILTER_REQUEST; constructor() { } } export class LoadFilterSuccess implements Action { - readonly type = LOAD_FILTER_SUCCESS; + readonly type = FilterCollectionActionTypes.LOAD_FILTER_SUCCESS; constructor(public payload: MetadataFilter[]) { } } export class LoadFilterError implements Action { - readonly type = LOAD_FILTER_ERROR; + readonly type = FilterCollectionActionTypes.LOAD_FILTER_ERROR; constructor(public payload: any) { } } export class UpdateFilterRequest implements Action { - readonly type = UPDATE_FILTER_REQUEST; + readonly type = FilterCollectionActionTypes.UPDATE_FILTER_REQUEST; constructor(public payload: MetadataFilter) { } } export class UpdateFilterSuccess implements Action { - readonly type = UPDATE_FILTER_SUCCESS; + readonly type = FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS; - constructor(public payload: MetadataFilter) { } + constructor(public payload: Update) { } } export class UpdateFilterFail implements Action { - readonly type = UPDATE_FILTER_FAIL; + readonly type = FilterCollectionActionTypes.UPDATE_FILTER_FAIL; constructor(public err: any) { } } export class AddFilterRequest implements Action { - readonly type = ADD_FILTER; + readonly type = FilterCollectionActionTypes.ADD_FILTER; constructor(public payload: MetadataFilter) { } } export class AddFilterSuccess implements Action { - readonly type = ADD_FILTER_SUCCESS; + readonly type = FilterCollectionActionTypes.ADD_FILTER_SUCCESS; constructor(public payload: MetadataFilter) { } } export class AddFilterFail implements Action { - readonly type = ADD_FILTER_FAIL; + readonly type = FilterCollectionActionTypes.ADD_FILTER_FAIL; constructor(public payload: any) { } } export class RemoveFilterRequest implements Action { - readonly type = REMOVE_FILTER; + readonly type = FilterCollectionActionTypes.REMOVE_FILTER; constructor(public payload: MetadataFilter) { } } export class RemoveFilterSuccess implements Action { - readonly type = REMOVE_FILTER_SUCCESS; + readonly type = FilterCollectionActionTypes.REMOVE_FILTER_SUCCESS; constructor(public payload: MetadataFilter) { } } export class RemoveFilterFail implements Action { - readonly type = REMOVE_FILTER_FAIL; + readonly type = FilterCollectionActionTypes.REMOVE_FILTER_FAIL; constructor(public payload: MetadataFilter) { } } -export type Actions = +export type FilterCollectionActionsUnion = | LoadFilterRequest | LoadFilterSuccess | LoadFilterError diff --git a/ui/src/app/domain/action/provider-collection.action.ts b/ui/src/app/domain/action/provider-collection.action.ts index 5c8720080..e93b36364 100644 --- a/ui/src/app/domain/action/provider-collection.action.ts +++ b/ui/src/app/domain/action/provider-collection.action.ts @@ -1,130 +1,133 @@ import { Action } from '@ngrx/store'; import { MetadataProvider } from '../model/metadata-provider'; +import { Update } from '@ngrx/entity'; -export const FIND = '[Metadata Provider] Find'; -export const SELECT = '[Metadata Provider] Select'; -export const SELECT_SUCCESS = '[Metadata Provider] Select Success'; +export enum ProviderCollectionActionTypes { + FIND = '[Metadata Provider] Find', + SELECT = '[Metadata Provider] Select', + SELECT_SUCCESS = '[Metadata Provider] Select Success', -export const UPDATE_PROVIDER_REQUEST = '[Metadata Provider] Update Request'; -export const UPDATE_PROVIDER_SUCCESS = '[Metadata Provider] Update Success'; -export const UPDATE_PROVIDER_FAIL = '[Metadata Provider] Update Fail'; + UPDATE_PROVIDER_REQUEST = '[Metadata Provider] Update Request', + UPDATE_PROVIDER_SUCCESS = '[Metadata Provider] Update Success', + UPDATE_PROVIDER_FAIL = '[Metadata Provider] Update Fail', -export const LOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Provider REQUEST'; -export const LOAD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Provider SUCCESS'; -export const LOAD_PROVIDER_ERROR = '[Metadata Provider Collection] Provider ERROR'; -export const ADD_PROVIDER = '[Metadata Provider Collection] Add Provider'; -export const ADD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Add Provider Success'; -export const ADD_PROVIDER_FAIL = '[Metadata Provider Collection] Add Provider Fail'; -export const REMOVE_PROVIDER = '[Metadata Provider Collection] Remove Provider'; -export const REMOVE_PROVIDER_SUCCESS = '[Metadata Provider Collection] Remove Provider Success'; -export const REMOVE_PROVIDER_FAIL = '[Metadata Provider Collection] Remove Provider Fail'; + LOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Provider REQUEST', + LOAD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Provider SUCCESS', + LOAD_PROVIDER_ERROR = '[Metadata Provider Collection] Provider ERROR', + ADD_PROVIDER = '[Metadata Provider Collection] Add Provider', + ADD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Add Provider Success', + ADD_PROVIDER_FAIL = '[Metadata Provider Collection] Add Provider Fail', + REMOVE_PROVIDER = '[Metadata Provider Collection] Remove Provider', + REMOVE_PROVIDER_SUCCESS = '[Metadata Provider Collection] Remove Provider Success', + REMOVE_PROVIDER_FAIL = '[Metadata Provider Collection] Remove Provider Fail', -export const UPLOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Upload Provider Request'; -export const CREATE_PROVIDER_FROM_URL_REQUEST = '[Metadata Provider Collection] Create Provider From URL Request'; + UPLOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Upload Provider Request', + CREATE_PROVIDER_FROM_URL_REQUEST = '[Metadata Provider Collection] Create Provider From URL Request', +} export class FindProvider implements Action { - readonly type = FIND; + readonly type = ProviderCollectionActionTypes.FIND; constructor(public payload: string) { } } export class SelectProvider implements Action { - readonly type = SELECT; + readonly type = ProviderCollectionActionTypes.SELECT; constructor(public payload: string) { } } export class SelectProviderSuccess implements Action { - readonly type = SELECT_SUCCESS; + readonly type = ProviderCollectionActionTypes.SELECT_SUCCESS; constructor(public payload: MetadataProvider) { } } export class LoadProviderRequest implements Action { - readonly type = LOAD_PROVIDER_REQUEST; + readonly type = ProviderCollectionActionTypes.LOAD_PROVIDER_REQUEST; constructor() { } } export class LoadProviderSuccess implements Action { - readonly type = LOAD_PROVIDER_SUCCESS; + readonly type = ProviderCollectionActionTypes.LOAD_PROVIDER_SUCCESS; constructor(public payload: MetadataProvider[]) { } } export class LoadProviderError implements Action { - readonly type = LOAD_PROVIDER_ERROR; + readonly type = ProviderCollectionActionTypes.LOAD_PROVIDER_ERROR; constructor(public payload: any) { } } export class UpdateProviderRequest implements Action { - readonly type = UPDATE_PROVIDER_REQUEST; + readonly type = ProviderCollectionActionTypes.UPDATE_PROVIDER_REQUEST; constructor(public payload: MetadataProvider) { } } export class UpdateProviderSuccess implements Action { - readonly type = UPDATE_PROVIDER_SUCCESS; + readonly type = ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS; - constructor(public payload: MetadataProvider) { } + constructor(public payload: Update) { } } export class UpdateProviderFail implements Action { - readonly type = UPDATE_PROVIDER_FAIL; + readonly type = ProviderCollectionActionTypes.UPDATE_PROVIDER_FAIL; constructor(public err: any) { } } export class AddProviderRequest implements Action { - readonly type = ADD_PROVIDER; + readonly type = ProviderCollectionActionTypes.ADD_PROVIDER; constructor(public payload: MetadataProvider) { } } export class AddProviderSuccess implements Action { - readonly type = ADD_PROVIDER_SUCCESS; + readonly type = ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS; constructor(public payload: MetadataProvider) { } } export class AddProviderFail implements Action { - readonly type = ADD_PROVIDER_FAIL; + readonly type = ProviderCollectionActionTypes.ADD_PROVIDER_FAIL; constructor(public payload: any) { } } export class RemoveProviderRequest implements Action { - readonly type = REMOVE_PROVIDER; + readonly type = ProviderCollectionActionTypes.REMOVE_PROVIDER; constructor(public payload: MetadataProvider) { } } export class RemoveProviderSuccess implements Action { - readonly type = REMOVE_PROVIDER_SUCCESS; + readonly type = ProviderCollectionActionTypes.REMOVE_PROVIDER_SUCCESS; constructor(public payload: MetadataProvider) { } } export class RemoveProviderFail implements Action { - readonly type = REMOVE_PROVIDER_FAIL; + readonly type = ProviderCollectionActionTypes.REMOVE_PROVIDER_FAIL; constructor(public payload: MetadataProvider) { } } export class UploadProviderRequest implements Action { - readonly type = UPLOAD_PROVIDER_REQUEST; + readonly type = ProviderCollectionActionTypes.UPLOAD_PROVIDER_REQUEST; constructor(public payload: { name: string, body: string }) { } } export class CreateProviderFromUrlRequest implements Action { - readonly type = CREATE_PROVIDER_FROM_URL_REQUEST; + readonly type = ProviderCollectionActionTypes.CREATE_PROVIDER_FROM_URL_REQUEST; constructor(public payload: { name: string, url: string }) { } } -export type Actions = +export type ProviderCollectionActionsUnion = | LoadProviderRequest | LoadProviderSuccess | LoadProviderError diff --git a/ui/src/app/domain/domain.module.ts b/ui/src/app/domain/domain.module.ts index ed433dd26..3c5f1bcf3 100644 --- a/ui/src/app/domain/domain.module.ts +++ b/ui/src/app/domain/domain.module.ts @@ -1,4 +1,5 @@ import { NgModule, ModuleWithProviders } from '@angular/core'; +import { HttpModule } from '@angular/http'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; @@ -13,11 +14,16 @@ import { DraftCollectionEffects } from './effect/draft-collection.effects'; import { ProviderCollectionEffects } from './effect/provider-collection.effects'; import { FilterCollectionEffects } from './effect/filter-collection.effect'; import { MetadataResolverService } from './service/metadata-resolver.service'; +import { EntityEffects } from './effect/entity.effect'; +import { PreviewDialogModule } from '../shared/preview/preview-dialog.module'; @NgModule({ declarations: [], entryComponents: [], - imports: [], + imports: [ + HttpModule, + PreviewDialogModule + ], exports: [], providers: [] }) @@ -42,7 +48,12 @@ export class DomainModule { imports: [ DomainModule, StoreModule.forFeature('collections', reducers), - EffectsModule.forFeature([FilterCollectionEffects, DraftCollectionEffects, ProviderCollectionEffects]) + EffectsModule.forFeature([ + FilterCollectionEffects, + DraftCollectionEffects, + ProviderCollectionEffects, + EntityEffects + ]) ], }) export class RootDomainModule { } diff --git a/ui/src/app/domain/effect/draft-collection.effects.ts b/ui/src/app/domain/effect/draft-collection.effects.ts index c83c68033..3fd5a430a 100644 --- a/ui/src/app/domain/effect/draft-collection.effects.ts +++ b/ui/src/app/domain/effect/draft-collection.effects.ts @@ -1,10 +1,13 @@ import { Injectable } from '@angular/core'; import { Effect, Actions } from '@ngrx/effects'; import { Action } from '@ngrx/store'; +import { Update } from '@ngrx/entity'; import { Observable } from 'rxjs/Observable'; import { Router } from '@angular/router'; -import * as draftActions from '../action/draft-collection.action'; +import { DraftCollectionActionTypes, DraftCollectionActionsUnion } from '../action/draft-collection.action'; +import * as actions from '../action/draft-collection.action'; + import { MetadataProvider } from '../../domain/model/metadata-provider'; import { EntityDraftService } from '../../domain/service/entity-draft.service'; @@ -13,75 +16,78 @@ export class DraftCollectionEffects { @Effect() loadDrafts$ = this.actions$ - .ofType(draftActions.LOAD_DRAFT_REQUEST) + .ofType(DraftCollectionActionTypes.LOAD_DRAFT_REQUEST) .switchMap(() => this.draftService .query() - .map(descriptors => new draftActions.LoadDraftSuccess(descriptors)) - .catch(error => Observable.of(new draftActions.LoadDraftError(error))) + .map(descriptors => new actions.LoadDraftSuccess(descriptors)) + .catch(error => Observable.of(new actions.LoadDraftError(error))) ); @Effect() addDraft$ = this.actions$ - .ofType(draftActions.ADD_DRAFT) + .ofType(DraftCollectionActionTypes.ADD_DRAFT) .map(action => action.payload) .switchMap(provider => { return this.draftService .save(provider) - .map(p => new draftActions.AddDraftSuccess(provider)); + .map(p => new actions.AddDraftSuccess(provider)); }); @Effect() addDraftSuccessReload$ = this.actions$ - .ofType(draftActions.ADD_DRAFT_SUCCESS) + .ofType(DraftCollectionActionTypes.ADD_DRAFT_SUCCESS) .map(action => action.payload) .switchMap(provider => this.draftService .find(provider.entityId) - .map(p => new draftActions.LoadDraftRequest()) + .map(p => new actions.LoadDraftRequest()) ); @Effect({ dispatch: false }) addDraftSuccessRedirect$ = this.actions$ - .ofType(draftActions.ADD_DRAFT_SUCCESS) + .ofType(DraftCollectionActionTypes.ADD_DRAFT_SUCCESS) .map(action => action.payload) .do(provider => this.router.navigate(['provider', provider.entityId, 'wizard'])); @Effect() updateDraft$ = this.actions$ - .ofType(draftActions.UPDATE_DRAFT_REQUEST) + .ofType(DraftCollectionActionTypes.UPDATE_DRAFT_REQUEST) .map(action => action.payload) .switchMap(provider => { return this.draftService .update(provider) - .map(p => new draftActions.UpdateDraftSuccess(p)); + .map(p => new actions.UpdateDraftSuccess({ + id: p.entityId, + changes: p + })); }); @Effect() selectDraft$ = this.actions$ - .ofType(draftActions.SELECT) + .ofType(DraftCollectionActionTypes.SELECT) .map(action => action.payload) .switchMap(id => this.draftService .find(id) - .map(p => new draftActions.FindDraft(p.entityId)) + .map(p => new actions.FindDraft(p.entityId)) ); @Effect() removeDraft$ = this.actions$ - .ofType(draftActions.REMOVE_DRAFT) + .ofType(DraftCollectionActionTypes.REMOVE_DRAFT) .map(action => action.payload) .switchMap(provider => this.draftService .remove(provider) - .map(p => new draftActions.RemoveDraftSuccess(p)) + .map(p => new actions.RemoveDraftSuccess(p)) ); @Effect() removeDraftSuccessReload$ = this.actions$ - .ofType(draftActions.REMOVE_DRAFT) + .ofType(DraftCollectionActionTypes.REMOVE_DRAFT) .map(action => action.payload) .map(provider => - new draftActions.LoadDraftRequest() + new actions.LoadDraftRequest() ); constructor( diff --git a/ui/src/app/domain/effect/entity.effect.ts b/ui/src/app/domain/effect/entity.effect.ts new file mode 100644 index 000000000..6e2e1edfc --- /dev/null +++ b/ui/src/app/domain/effect/entity.effect.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { Observable } from 'rxjs/Observable'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { fromPromise } from 'rxjs/observable/fromPromise'; + +import { EntityDescriptorService } from '../service/entity-descriptor.service'; +import { MetadataResolverService } from '../service/metadata-resolver.service'; +import { PreviewDialogComponent } from '../../shared/preview/preview-dialog.component'; +import { MetadataEntity, DomainTypes } from '../domain.type'; +import { EntityIdService } from '../service/entity-id.service'; +import * as entityActions from '../action/entity.action'; + + +@Injectable() +export class EntityEffects { + + @Effect({ dispatch: false }) + previewEntityXml$ = this.actions$ + .ofType(entityActions.PREVIEW_ENTITY) + .pipe( + map(action => action.payload), + tap(entity => this.openModal(entity)) + ); + + constructor( + private actions$: Actions, + private modalService: NgbModal, + private providerService: EntityDescriptorService, + private entityService: EntityIdService + ) { } + + openModal(entity: MetadataEntity): void { + let request: Observable = entity.type === DomainTypes.filter ? + this.entityService.preview(entity.entityId) : this.providerService.preview(entity.id); + request.subscribe(xml => { + let modal = this.modalService.open(PreviewDialogComponent, { + size: 'lg', + windowClass: 'modal-xl' + }); + modal.componentInstance.entity = entity; + modal.componentInstance.xml = xml; + }); + } +} /* istanbul ignore next */ diff --git a/ui/src/app/domain/effect/filter-collection.effect.ts b/ui/src/app/domain/effect/filter-collection.effect.ts index 6ea5f295d..23475858f 100644 --- a/ui/src/app/domain/effect/filter-collection.effect.ts +++ b/ui/src/app/domain/effect/filter-collection.effect.ts @@ -12,6 +12,7 @@ import 'rxjs/add/operator/switchMap'; import 'rxjs/add/operator/withLatestFrom'; import * as actions from '../action/filter-collection.action'; +import { FilterCollectionActionTypes, FilterCollectionActionsUnion } from '../action/filter-collection.action'; import * as fromFilter from '../reducer'; import { EntityIdService } from '../../domain/service/entity-id.service'; @@ -24,7 +25,7 @@ export class FilterCollectionEffects { @Effect() loadFilters$ = this.actions$ - .ofType(actions.LOAD_FILTER_REQUEST) + .ofType(FilterCollectionActionTypes.LOAD_FILTER_REQUEST) .switchMap(() => this.resolverService .query() @@ -33,7 +34,7 @@ export class FilterCollectionEffects { ); @Effect() selectFilterRequest$ = this.actions$ - .ofType(actions.SELECT) + .ofType(FilterCollectionActionTypes.SELECT) .map(action => action.payload) .switchMap(id => this.resolverService @@ -44,7 +45,7 @@ export class FilterCollectionEffects { @Effect() addFilter$ = this.actions$ - .ofType(actions.ADD_FILTER) + .ofType(FilterCollectionActionTypes.ADD_FILTER) .map(action => action.payload) .map(filter => { return { @@ -60,7 +61,7 @@ export class FilterCollectionEffects { ); @Effect({ dispatch: false }) addFilterSuccessRedirect$ = this.actions$ - .ofType(actions.ADD_FILTER_SUCCESS) + .ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS) .map(action => action.payload) .do(filter => { this.router.navigate(['/dashboard']); @@ -68,32 +69,35 @@ export class FilterCollectionEffects { @Effect() addFilterSuccessReload$ = this.actions$ - .ofType(actions.ADD_FILTER_SUCCESS) + .ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS) .map(action => action.payload) .map(filter => new actions.LoadFilterRequest()); @Effect() updateFilter$ = this.actions$ - .ofType(actions.UPDATE_FILTER_REQUEST) + .ofType(FilterCollectionActionTypes.UPDATE_FILTER_REQUEST) .map(action => action.payload) .switchMap(filter => { delete filter.modifiedDate; delete filter.createdDate; return this.resolverService .update(filter) - .map(p => new actions.UpdateFilterSuccess(p)) + .map(p => new actions.UpdateFilterSuccess({ + id: p.id, + changes: p + })) .catch(err => Observable.of(new actions.UpdateFilterFail(err))); }); @Effect({ dispatch: false }) updateFilterSuccessRedirect$ = this.actions$ - .ofType(actions.UPDATE_FILTER_SUCCESS) + .ofType(FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS) .map(action => action.payload) .do(filter => { this.router.navigate(['/dashboard']); }); @Effect() updateFilterSuccessReload$ = this.actions$ - .ofType(actions.UPDATE_FILTER_SUCCESS) + .ofType(FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS) .map(action => action.payload) .map(filter => new actions.LoadFilterRequest()); diff --git a/ui/src/app/domain/effect/provider-collection.effects.ts b/ui/src/app/domain/effect/provider-collection.effects.ts index 8b280e61e..0b7e57453 100644 --- a/ui/src/app/domain/effect/provider-collection.effects.ts +++ b/ui/src/app/domain/effect/provider-collection.effects.ts @@ -6,6 +6,7 @@ import { Router } from '@angular/router'; import * as providerActions from '../action/provider-collection.action'; import * as draftActions from '../action/draft-collection.action'; +import { ProviderCollectionActionsUnion, ProviderCollectionActionTypes } from '../action/provider-collection.action'; import { MetadataProvider } from '../../domain/model/metadata-provider'; import { EntityDescriptorService } from '../../domain/service/entity-descriptor.service'; import { removeNulls } from '../../shared/util'; @@ -15,7 +16,7 @@ export class ProviderCollectionEffects { @Effect() loadProviders$ = this.actions$ - .ofType(providerActions.LOAD_PROVIDER_REQUEST) + .ofType(ProviderCollectionActionTypes.LOAD_PROVIDER_REQUEST) .switchMap(() => this.descriptorService .query() @@ -25,32 +26,35 @@ export class ProviderCollectionEffects { @Effect() updateProvider$ = this.actions$ - .ofType(providerActions.UPDATE_PROVIDER_REQUEST) + .ofType(ProviderCollectionActionTypes.UPDATE_PROVIDER_REQUEST) .map(action => action.payload) .switchMap(provider => { delete provider.modifiedDate; delete provider.createdDate; return this.descriptorService .update(provider) - .map(p => new providerActions.UpdateProviderSuccess(p)) + .map(p => new providerActions.UpdateProviderSuccess({ + id: p.id, + changes: p + })) .catch(err => Observable.of(new providerActions.UpdateProviderFail(err))); }); @Effect({ dispatch: false }) updateProviderSuccessRedirect$ = this.actions$ - .ofType(providerActions.UPDATE_PROVIDER_SUCCESS) + .ofType(ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS) .map(action => action.payload) .do(provider => { this.router.navigate(['/dashboard']); }); @Effect() updateProviderSuccessReload$ = this.actions$ - .ofType(providerActions.UPDATE_PROVIDER_SUCCESS) + .ofType(ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS) .map(action => action.payload) .map(provider => new providerActions.LoadProviderRequest()); @Effect() selectProvider$ = this.actions$ - .ofType(providerActions.SELECT) + .ofType(ProviderCollectionActionTypes.SELECT) .map(action => action.payload) .switchMap(id => this.descriptorService @@ -60,7 +64,7 @@ export class ProviderCollectionEffects { @Effect() addProviderRequest$ = this.actions$ - .ofType(providerActions.ADD_PROVIDER) + .ofType(ProviderCollectionActionTypes.ADD_PROVIDER) .map(action => action.payload) .map(provider => { return { @@ -77,26 +81,26 @@ export class ProviderCollectionEffects { @Effect({ dispatch: false }) addProviderSuccessRedirect$ = this.actions$ - .ofType(providerActions.ADD_PROVIDER_SUCCESS) + .ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS) .map(action => action.payload) .do(provider => { this.router.navigate(['/dashboard']); }); @Effect() addProviderSuccessReload$ = this.actions$ - .ofType(providerActions.ADD_PROVIDER_SUCCESS) + .ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS) .map(action => action.payload) .map(provider => new providerActions.LoadProviderRequest()); @Effect() addProviderSuccessRemoveDraft$ = this.actions$ - .ofType(providerActions.ADD_PROVIDER_SUCCESS) + .ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS) .map(action => action.payload) .map(provider => new draftActions.RemoveDraftRequest(provider)); @Effect() uploadProviderRequest$ = this.actions$ - .ofType(providerActions.UPLOAD_PROVIDER_REQUEST) + .ofType(ProviderCollectionActionTypes.UPLOAD_PROVIDER_REQUEST) .map(action => action.payload) .switchMap(file => this.descriptorService @@ -107,7 +111,7 @@ export class ProviderCollectionEffects { @Effect() createProviderFromUrlRequest$ = this.actions$ - .ofType(providerActions.CREATE_PROVIDER_FROM_URL_REQUEST) + .ofType(ProviderCollectionActionTypes.CREATE_PROVIDER_FROM_URL_REQUEST) .map(action => action.payload) .switchMap(file => this.descriptorService diff --git a/ui/src/app/domain/entity/filter.ts b/ui/src/app/domain/entity/filter.ts index c8744ff9e..03860fcdd 100644 --- a/ui/src/app/domain/entity/filter.ts +++ b/ui/src/app/domain/entity/filter.ts @@ -18,12 +18,12 @@ export class Filter implements MetadataFilter { attributeRelease = [] as string[]; filterTarget: FilterTarget = { - type: 'entity', - value: '' + type: 'ENTITY', + value: [''] }; constructor(obj?: Partial) { - Object.assign(this, obj || {}); + Object.assign(this, { ...obj }); } get name(): string { @@ -39,10 +39,20 @@ export class Filter implements MetadataFilter { } get entityId(): string { - return this.filterTarget.value; + return this.filterTarget.value[0]; } set entityId(val: string) { - this.filterTarget.value = val; + this.filterTarget.value[0] = val; + } + + serialize(): any { + return { + attributeRelease: this.attributeRelease, + relyingPartyOverrides: this.relyingPartyOverrides, + filterTarget: { ...this.filterTarget }, + filterEnabled: this.filterEnabled, + filterName: this.filterName + }; } } diff --git a/ui/src/app/domain/entity/provider.ts b/ui/src/app/domain/entity/provider.ts index 9f495b69a..4adbd32e1 100644 --- a/ui/src/app/domain/entity/provider.ts +++ b/ui/src/app/domain/entity/provider.ts @@ -61,4 +61,8 @@ export class Provider implements MetadataProvider { get type(): string { return DomainTypes.provider; } + + serialize(): any { + return this; + } } diff --git a/ui/src/app/domain/guard/filter-exists.guard.ts b/ui/src/app/domain/guard/filter-exists.guard.ts index 2034e8ab6..663038b98 100644 --- a/ui/src/app/domain/guard/filter-exists.guard.ts +++ b/ui/src/app/domain/guard/filter-exists.guard.ts @@ -22,23 +22,14 @@ export class FilterExistsGuard implements CanActivate { private router: Router ) { } - /** - * This method creates an observable that waits for the `loaded` property - * of the collection state to turn `true`, emitting one time once loading - * has finished. - */ waitForCollectionToLoad(): Observable { return this.store.pipe( - select(fromCollection.getFilterCollectionLoaded), + select(fromCollection.getFilterCollectionIsLoaded), filter(loaded => loaded), take(1) ); } - /** - * This method checks if a filter with the given ID is already registered - * in the Store - */ hasFilterInStore(id: string): Observable { return this.store.pipe( select(fromCollection.getFilterEntities), @@ -47,10 +38,6 @@ export class FilterExistsGuard implements CanActivate { ); } - /** - * This method loads a filter with the given ID from the API and caches - * it in the store, returning `true` or `false` if it was found. - */ hasFilterInApi(id: string): Observable { return this.mdResolverService.find(id).pipe( map(filterEntity => new FilterActions.LoadFilterSuccess([filterEntity])), @@ -63,11 +50,6 @@ export class FilterExistsGuard implements CanActivate { ); } - /** - * `hasFilter` composes `hasFilterInStore` and `hasFilterInApi`. It first checks - * if the filter is in store, and if not it then checks if it is in the - * API. - */ hasFilter(id: string): Observable { return this.hasFilterInStore(id).pipe( switchMap(inStore => { @@ -79,20 +61,6 @@ export class FilterExistsGuard implements CanActivate { }) ); } - - /** - * This is the actual method the router will call when our guard is run. - * - * Our guard waits for the collection to load, then it checks if we need - * to request a filter from the API or if we already have it in our cache. - * If it finds it in the cache or in the API, it returns an Observable - * of `true` and the route is rendered successfully. - * - * If it was unable to find it in our cache or in the API, this guard - * will return an Observable of `false`, causing the router to move - * on to the next candidate route. In this case, it will move on - * to the 404 page. - */ canActivate(route: ActivatedRouteSnapshot): Observable { return this.waitForCollectionToLoad().pipe( switchMap(() => this.hasFilter(route.params['id'])) diff --git a/ui/src/app/domain/model/filter-target.ts b/ui/src/app/domain/model/filter-target.ts index 3b8b59856..d1e7ad9ca 100644 --- a/ui/src/app/domain/model/filter-target.ts +++ b/ui/src/app/domain/model/filter-target.ts @@ -1,4 +1,4 @@ export interface FilterTarget { type: string; - value: string; + value: string[]; } diff --git a/ui/src/app/domain/model/metadata-base.ts b/ui/src/app/domain/model/metadata-base.ts index 2b5c8d17a..e3b75e1ca 100644 --- a/ui/src/app/domain/model/metadata-base.ts +++ b/ui/src/app/domain/model/metadata-base.ts @@ -14,4 +14,6 @@ export interface MetadataBase { name: string; enabled: boolean; type: string; + + serialize(): any; } diff --git a/ui/src/app/domain/model/metadata-filter.ts b/ui/src/app/domain/model/metadata-filter.ts index 2351cdbc6..548d6d813 100644 --- a/ui/src/app/domain/model/metadata-filter.ts +++ b/ui/src/app/domain/model/metadata-filter.ts @@ -9,6 +9,8 @@ export interface MetadataFilter extends MetadataBase { relyingPartyOverrides: RelyingPartyOverrides; attributeRelease: string[]; filterTarget: FilterTarget; + + serialize(): any; } export * from './relying-party-overrides'; diff --git a/ui/src/app/domain/reducer/draft-collection.reducer.spec.ts b/ui/src/app/domain/reducer/draft-collection.reducer.spec.ts index 584ce8161..e9f72bd73 100644 --- a/ui/src/app/domain/reducer/draft-collection.reducer.spec.ts +++ b/ui/src/app/domain/reducer/draft-collection.reducer.spec.ts @@ -1,15 +1,15 @@ -import { reducer } from './draft-collection.reducer'; +import { reducer, adapter } from './draft-collection.reducer'; import * as fromDrafts from './draft-collection.reducer'; import * as draftActions from '../action/draft-collection.action'; import { MetadataProvider } from '../../domain/model/metadata-provider'; let drafts: MetadataProvider[] = [ - { entityId: 'foo', serviceProviderName: 'bar' } as MetadataProvider, - { entityId: 'baz', serviceProviderName: 'fin' } as MetadataProvider -], + { entityId: 'foo', serviceProviderName: 'bar' } as MetadataProvider, + { entityId: 'baz', serviceProviderName: 'fin' } as MetadataProvider + ], snapshot: fromDrafts.DraftCollectionState = { ids: [drafts[0].entityId, drafts[1].entityId], - drafts: { + entities: { [drafts[0].entityId]: drafts[0], [drafts[1].entityId]: drafts[1] }, @@ -19,7 +19,7 @@ let drafts: MetadataProvider[] = [ describe('Draft Reducer', () => { const initialState: fromDrafts.DraftCollectionState = { ids: [], - drafts: {}, + entities: {}, selectedDraftId: null }; @@ -44,26 +44,25 @@ describe('Draft Reducer', () => { describe('Update Drafts: Success', () => { it('should update the draft of the specified entityId', () => { - let changes = { ...drafts[1], serviceEnabled: true }, + let changes = { ...drafts[1], serviceProviderName: 'foo' }, expected = { ids: [drafts[0].entityId, drafts[1].entityId], - drafts: { + entities: { [drafts[0].entityId]: drafts[0], [drafts[1].entityId]: changes }, selectedDraftId: null }; - const action = new draftActions.UpdateDraftSuccess(changes); + spyOn(adapter, 'updateOne'); + const action = new draftActions.UpdateDraftSuccess({id: changes.id, changes }); const result = reducer({ ...snapshot }, action); - expect(result).toEqual( - Object.assign({}, initialState, expected) - ); + expect(adapter.updateOne).toHaveBeenCalled(); }); it('should return state if the entityId is not found', () => { let changes = { ...drafts[1], serviceEnabled: true, entityId: 'bar' }; - const action = new draftActions.UpdateDraftSuccess(changes); + const action = new draftActions.UpdateDraftSuccess({id: changes.id, changes}); const result = reducer({ ...snapshot }, action); expect(result).toEqual(snapshot); @@ -84,38 +83,32 @@ describe('Draft Reducer', () => { }); describe('Selectors', () => { it('getEntities should return all drafts', () => { - expect(fromDrafts.getEntities({ + expect(fromDrafts.selectDraftEntities({ ids: [], - drafts: {}, - selectedDraftId: null, + entities: {}, })).toEqual({}); - expect(fromDrafts.getEntities(snapshot)).toEqual(snapshot.drafts); + expect(fromDrafts.selectDraftEntities(snapshot)).toEqual(snapshot.entities); }); it('getIds should return all Ids', () => { - expect(fromDrafts.getIds({ + expect(fromDrafts.selectDraftIds({ ids: [], - drafts: {}, - selectedDraftId: null, + entities: {} })).toEqual([]); - expect(fromDrafts.getIds(snapshot)).toEqual(snapshot.ids); + expect(fromDrafts.selectDraftIds(snapshot)).toEqual(snapshot.ids); }); it('getSelectedDraftId should return the selected entityId', () => { - expect(fromDrafts.getSelectedId({ + expect(fromDrafts.getSelectedDraftId({ ids: [], - drafts: {}, + entities: {}, selectedDraftId: null, })).toBeNull(); - expect(fromDrafts.getSelectedId(Object.assign({}, snapshot, { selectedDraftId: 'foo' }))).toEqual('foo'); - }); - - it('getSelected should return the selected entity by id', () => { - expect(fromDrafts.getSelected(Object.assign({}, snapshot, { selectedDraftId: 'foo' }))).toEqual(drafts[0]); + expect(fromDrafts.getSelectedDraftId(Object.assign({}, snapshot, { selectedDraftId: 'foo' }))).toEqual('foo'); }); it('getAll return all entities as an array', () => { - expect(fromDrafts.getAll(snapshot)).toEqual(drafts); + expect(fromDrafts.selectAllDrafts(snapshot)).toEqual(drafts); }); }); }); diff --git a/ui/src/app/domain/reducer/draft-collection.reducer.ts b/ui/src/app/domain/reducer/draft-collection.reducer.ts index dace979c3..0fea3ea3d 100644 --- a/ui/src/app/domain/reducer/draft-collection.reducer.ts +++ b/ui/src/app/domain/reducer/draft-collection.reducer.ts @@ -1,67 +1,47 @@ import { createSelector } from '@ngrx/store'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + import { MetadataProvider } from '../../domain/model/metadata-provider'; import * as providerAction from '../action/provider-collection.action'; -import * as draftAction from '../action/draft-collection.action'; +import { DraftCollectionActionsUnion, DraftCollectionActionTypes } from '../action/draft-collection.action'; -export interface DraftCollectionState { - ids: string[]; - drafts: { [id: string]: MetadataProvider }; +export interface DraftCollectionState extends EntityState { selectedDraftId: string | null; } -export const initialState: DraftCollectionState = { - ids: [], - drafts: {}, - selectedDraftId: null, -}; +export function sortByName(a: MetadataProvider, b: MetadataProvider): number { + return a.serviceProviderName.localeCompare(b.serviceProviderName); +} -export function reducer(state = initialState, action: draftAction.Actions): DraftCollectionState { - switch (action.type) { - case draftAction.LOAD_DRAFT_SUCCESS: { - const providers = action.payload; +export const adapter: EntityAdapter = createEntityAdapter({ + sortComparer: sortByName, + selectId: (model: MetadataProvider) => model.entityId +}); - const providerIds = providers.map(provider => provider.entityId); - const entities = providers.reduce( - (e: { [id: string]: MetadataProvider }, provider: MetadataProvider) => { - return Object.assign(e, { - [provider.entityId]: provider, - }); - }, - {} - ); +export const initialState: DraftCollectionState = adapter.getInitialState({ + selectedDraftId: null, +}); - return { - ids: [...providerIds], - drafts: Object.assign(entities), +export function reducer(state = initialState, action: DraftCollectionActionsUnion): DraftCollectionState { + switch (action.type) { + case DraftCollectionActionTypes.LOAD_DRAFT_SUCCESS: { + return adapter.addMany(action.payload, { + ...state, selectedDraftId: state.selectedDraftId, - }; + }); } - case draftAction.UPDATE_DRAFT_SUCCESS: { - const draft = action.payload; - - if (state.ids.indexOf(draft.entityId) < 0) { - return state; - } + case DraftCollectionActionTypes.UPDATE_DRAFT_SUCCESS: { + return adapter.updateOne(action.payload, state); + } - const original = state.drafts[draft.entityId], - updated = Object.assign({}, - { ...original }, - { ...draft } - ); - return { - ids: state.ids, - drafts: Object.assign({ ...state.drafts }, { - [draft.entityId]: updated, - }), - selectedDraftId: state.selectedDraftId, - }; + case DraftCollectionActionTypes.REMOVE_DRAFT_SUCCESS: { + return adapter.removeOne(action.payload.entityId, state); } - case draftAction.SELECT: { + case DraftCollectionActionTypes.SELECT: { return { - ids: state.ids, - drafts: state.drafts, + ...state, selectedDraftId: action.payload, }; } @@ -72,16 +52,10 @@ export function reducer(state = initialState, action: draftAction.Actions): Draf } } -export const getEntities = (state: DraftCollectionState) => state.drafts; -export const getIds = (state: DraftCollectionState) => state.ids; -export const getSelectedId = (state: DraftCollectionState) => state.selectedDraftId; -export const getSelected = createSelector( - getEntities, - getSelectedId, - (entities, selectedId) => { - return entities[selectedId]; - } -); -export const getAll = createSelector(getEntities, getIds, (entities, ids) => { - return ids.map(id => entities[id]); -}); +export const getSelectedDraftId = (state: DraftCollectionState) => state.selectedDraftId; +export const { + selectIds: selectDraftIds, + selectEntities: selectDraftEntities, + selectAll: selectAllDrafts, + selectTotal: selectDraftTotal +} = adapter.getSelectors(); diff --git a/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts b/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts index c697d31a8..0fdfa326a 100644 --- a/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts +++ b/ui/src/app/domain/reducer/filter-collection.reducer.spec.ts @@ -5,7 +5,7 @@ import * as actions from '../action/filter-collection.action'; const snapshot: fromFilter.FilterCollectionState = { ids: [], entities: {}, - selectedId: null, + selectedFilterId: null, loaded: false }; diff --git a/ui/src/app/domain/reducer/filter-collection.reducer.ts b/ui/src/app/domain/reducer/filter-collection.reducer.ts index b87de3c09..3a5fa18c8 100644 --- a/ui/src/app/domain/reducer/filter-collection.reducer.ts +++ b/ui/src/app/domain/reducer/filter-collection.reducer.ts @@ -1,84 +1,46 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; import * as filter from '../action/filter-collection.action'; -import * as fromRoot from '../../core/reducer'; +import { FilterCollectionActionTypes, FilterCollectionActionsUnion } from '../action/filter-collection.action'; import { MetadataFilter } from '../domain.type'; -export interface FilterCollectionState { - ids: string[]; - entities: { [id: string]: MetadataFilter }; - selectedId: string | null; +export interface FilterCollectionState extends EntityState { + selectedFilterId: string | null; loaded: boolean; } -export const initialState: FilterCollectionState = { - ids: [], - entities: {}, - selectedId: null, - loaded: false -}; +export function sortByDate(a: MetadataFilter, b: MetadataFilter): number { + return a.createdDate.localeCompare(b.createdDate); +} -export function reducer(state = initialState, action: filter.Actions): FilterCollectionState { - switch (action.type) { - case filter.LOAD_FILTER_SUCCESS: { - const filters = action.payload; +export const adapter: EntityAdapter = createEntityAdapter({ + sortComparer: sortByDate, + selectId: (model: MetadataFilter) => model.id +}); - const filterIds = filters.map(provider => provider.id); - const entities = filters.reduce( - (e: { [id: string]: MetadataFilter }, provider: MetadataFilter) => { - return Object.assign(e, { - [provider.id]: provider, - }); - }, - {} - ); +export const initialState: FilterCollectionState = adapter.getInitialState({ + selectedFilterId: null, + loaded: false +}); - return { +export function reducer(state = initialState, action: FilterCollectionActionsUnion): FilterCollectionState { + switch (action.type) { + case FilterCollectionActionTypes.LOAD_FILTER_SUCCESS: { + return adapter.addMany(action.payload, { ...state, - ids: [...filterIds], - entities: { ...state.entities, ...entities }, + selectedFilterId: state.selectedFilterId, loaded: true - }; + }); } - case filter.SELECT_FILTER_SUCCESS: { - const filter = action.payload; - if (!filter || state.ids.indexOf(filter.id) < 0) { - return state; - } - return { - ...state, - ids: [...state.ids.filter(id => id !== filter.id), filter.id], - entities: Object.assign({ ...state.entities }, { - [filter.id]: filter, - }), - selectedId: filter.id - }; + case FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS: { + return adapter.updateOne(action.payload, state); } - case filter.UPDATE_FILTER_SUCCESS: { - const provider = action.payload; - - if (state.ids.indexOf(provider.id) < 0) { - return state; - } - const original = state.entities[provider.id], - updated = Object.assign({}, - { ...original }, - { ...provider } - ); - return { - ...state, - ids: [...state.ids.filter(id => id !== provider.id), provider.id], - entities: Object.assign({ ...state.entities }, { - [provider.id]: provider, - }) - }; - } - - case filter.SELECT: { + case FilterCollectionActionTypes.SELECT: { return { ...state, - selectedId: action.payload, + selectedFilterId: action.payload, }; } @@ -88,21 +50,11 @@ export function reducer(state = initialState, action: filter.Actions): FilterCol } } -export const getFilters = (state: FilterCollectionState) => state.entities; - -export const getEntities = (state: FilterCollectionState) => state.entities; -export const getIds = (state: FilterCollectionState) => state.ids; -export const getSelectedId = (state: FilterCollectionState) => state.selectedId; -export const getLoaded = (state: FilterCollectionState) => state.loaded; - -export const getSelected = createSelector( - getEntities, - getSelectedId, - (entities, selectedId) => { - return entities[selectedId]; - } -); - -export const getAll = createSelector(getEntities, getIds, (entities, ids) => { - return ids.map(id => entities[id]).filter(entity => entity); -}); +export const getSelectedFilterId = (state: FilterCollectionState) => state.selectedFilterId; +export const getIsLoaded = (state: FilterCollectionState) => state.loaded; +export const { + selectIds: selectFilterIds, + selectEntities: selectFilterEntities, + selectAll: selectAllFilters, + selectTotal: selectFilterTotal +} = adapter.getSelectors(); diff --git a/ui/src/app/domain/reducer/index.ts b/ui/src/app/domain/reducer/index.ts index f628093d0..a6281c90b 100644 --- a/ui/src/app/domain/reducer/index.ts +++ b/ui/src/app/domain/reducer/index.ts @@ -39,28 +39,31 @@ export const getDraftEntityState = createSelector(getCollectionState, getDraftsS export const combineAllFn = (d, p) => [...p, ...d]; export const doesExistFn = (ids, selected) => ids.indexOf(selected) > -1; -export const getInCollectionFn = (entities, selectedId) => selectedId && entities[selectedId]; +export const getInCollectionFn = (entities, selectedId) => { + return selectedId && entities[selectedId]; +}; export const getEntityIdsFn = list => list.map(entity => entity.entityId); /* * Select pieces of Provider Collection */ -export const getProviderEntities = createSelector(getProviderEntityState, fromProvider.getEntities); -export const getSelectedProviderId = createSelector(getProviderEntityState, fromProvider.getSelectedId); +export const getProviderEntities = createSelector(getProviderEntityState, fromProvider.selectProviderEntities); +export const getSelectedProviderId = createSelector(getProviderEntityState, fromProvider.getSelectedProviderId); export const getSelectedProvider = createSelector(getProviderEntities, getSelectedProviderId, getInCollectionFn); -export const getProviderIds = createSelector(getProviderEntityState, fromProvider.getIds); -export const getProviderCollection = createSelector(getProviderEntityState, getProviderIds, fromProvider.getAll); +export const getProviderIds = createSelector(getProviderEntityState, fromProvider.selectProviderIds); +export const getProviderCollection = createSelector(getProviderEntityState, getProviderIds, fromProvider.selectAllProviders); /* * Select pieces of Draft Collection */ -export const getDraftEntities = createSelector(getDraftEntityState, fromDraft.getEntities); -export const getSelectedDraftId = createSelector(getDraftEntityState, fromDraft.getSelectedId); +export const getDraftEntities = createSelector(getDraftEntityState, fromDraft.selectDraftEntities); +export const getDraftIds = createSelector(getDraftEntityState, fromDraft.selectDraftIds); +export const getDraftCollection = createSelector(getDraftEntityState, getDraftIds, fromDraft.selectAllDrafts); + +export const getSelectedDraftId = createSelector(getDraftEntityState, fromDraft.getSelectedDraftId); export const getSelectedDraft = createSelector(getDraftEntities, getSelectedDraftId, getInCollectionFn); -export const getDraftIds = createSelector(getDraftEntityState, fromDraft.getIds); -export const getDraftCollection = createSelector(getDraftEntityState, getDraftIds, fromDraft.getAll); export const isSelectedProviderInCollection = createSelector(getProviderIds, getSelectedProviderId, doesExistFn); export const isSelectedDraftInCollection = createSelector(getDraftIds, getSelectedDraftId, doesExistFn); @@ -68,12 +71,12 @@ export const isSelectedDraftInCollection = createSelector(getDraftIds, getSelect * Select pieces of Filter Collection */ -export const getAllFilters = createSelector(getFilterEntityState, fromFilter.getAll); -export const getFilterEntities = createSelector(getFilterEntityState, fromFilter.getEntities); -export const getSelectedFilterId = createSelector(getFilterEntityState, fromFilter.getSelectedId); +export const getAllFilters = createSelector(getFilterEntityState, fromFilter.selectAllFilters); +export const getFilterEntities = createSelector(getFilterEntityState, fromFilter.selectFilterEntities); +export const getSelectedFilterId = createSelector(getFilterEntityState, fromFilter.getSelectedFilterId); export const getSelectedFilter = createSelector(getFilterEntities, getSelectedFilterId, getInCollectionFn); -export const getFilterIds = createSelector(getFilterEntityState, fromFilter.getIds); -export const getFilterCollectionLoaded = createSelector(getFilterEntityState, fromFilter.getLoaded); +export const getFilterIds = createSelector(getFilterEntityState, fromFilter.selectFilterIds); +export const getFilterCollectionIsLoaded = createSelector(getFilterEntityState, fromFilter.getIsLoaded); /* * Combine pieces of Collection State diff --git a/ui/src/app/domain/reducer/provider-collection.reducer.spec.ts b/ui/src/app/domain/reducer/provider-collection.reducer.spec.ts index 09a34ed9a..baddf7317 100644 --- a/ui/src/app/domain/reducer/provider-collection.reducer.spec.ts +++ b/ui/src/app/domain/reducer/provider-collection.reducer.spec.ts @@ -4,8 +4,8 @@ import * as providerActions from '../action/provider-collection.action'; import { MetadataProvider } from '../model/metadata-provider'; let providers: MetadataProvider[] = [ - { id: '1', entityId: 'foo', serviceProviderName: 'bar' } as MetadataProvider, - { id: '2', entityId: 'baz', serviceProviderName: 'fin' } as MetadataProvider + { id: '1', entityId: 'foo', serviceProviderName: 'bar', createdDate: 'Tue Apr 17 2018 13:33:54 GMT-0700 (MST)' } as MetadataProvider, + { id: '2', entityId: 'baz', serviceProviderName: 'fin', createdDate: 'Tue Apr 17 2018 13:34:07 GMT-0700 (MST)' } as MetadataProvider ], snapshot: fromProvider.ProviderCollectionState = { ids: [providers[0].id, providers[1].id], @@ -53,7 +53,7 @@ describe('Provider Reducer', () => { }, selectedProviderId: null }; - const action = new providerActions.UpdateProviderSuccess(changes); + const action = new providerActions.UpdateProviderSuccess({id: changes.id, changes}); const result = reducer({ ...snapshot }, action); expect(result).toEqual( @@ -63,7 +63,7 @@ describe('Provider Reducer', () => { it('should return state if the entityId is not found', () => { let changes = { ...providers[1], serviceEnabled: true, id: '4' }; - const action = new providerActions.UpdateProviderSuccess(changes); + const action = new providerActions.UpdateProviderSuccess({id: changes.id, changes}); const result = reducer({ ...snapshot }, action); expect(result).toEqual(snapshot); @@ -82,28 +82,4 @@ describe('Provider Reducer', () => { ); }); }); - - describe('Select Provider Success', () => { - it('should update the selected draft id', () => { - let id = providers[0].id, - expected = { ...snapshot, selectedProviderId: id, ids: ['2', '1'] }; - const action = new providerActions.SelectProviderSuccess(providers[0]); - const result = reducer({ ...snapshot }, action); - - expect(result).toEqual( - Object.assign({}, initialState, expected) - ); - }); - - it('should not update state if the provider id does not exist', () => { - let id = providers[0].id, - expected = snapshot; - const action = new providerActions.SelectProviderSuccess({ ...providers[0], id: 'foo' }); - const result = reducer({ ...snapshot }, action); - - expect(result).toEqual( - Object.assign({}, initialState, expected) - ); - }); - }); }); diff --git a/ui/src/app/domain/reducer/provider-collection.reducer.ts b/ui/src/app/domain/reducer/provider-collection.reducer.ts index 309807da5..ddc301eb8 100644 --- a/ui/src/app/domain/reducer/provider-collection.reducer.ts +++ b/ui/src/app/domain/reducer/provider-collection.reducer.ts @@ -1,77 +1,41 @@ import { createSelector } from '@ngrx/store'; +import { EntityState, EntityAdapter, createEntityAdapter } from '@ngrx/entity'; + import { MetadataProvider } from '../../domain/model/metadata-provider'; import * as provider from '../action/provider-collection.action'; +import { ProviderCollectionActionsUnion, ProviderCollectionActionTypes } from '../action/provider-collection.action'; -export interface ProviderCollectionState { - ids: string[]; - entities: { [id: string]: MetadataProvider }; +export interface ProviderCollectionState extends EntityState { selectedProviderId: string | null; } -export const initialState: ProviderCollectionState = { - ids: [], - entities: {}, - selectedProviderId: null -}; +export function sortByDate(a: MetadataProvider, b: MetadataProvider): number { + return a.createdDate.localeCompare(b.createdDate); +} -export function reducer(state = initialState, action: provider.Actions): ProviderCollectionState { - switch (action.type) { - case provider.LOAD_PROVIDER_SUCCESS: { - const providers = action.payload; +export const adapter: EntityAdapter = createEntityAdapter({ + sortComparer: sortByDate, + selectId: (model: MetadataProvider) => model.id +}); - const providerIds = providers.map(provider => provider.id); - const entities = providers.reduce( - (e: { [id: string]: MetadataProvider }, provider: MetadataProvider) => { - return Object.assign(e, { - [provider.id]: provider, - }); - }, - {} - ); +export const initialState: ProviderCollectionState = adapter.getInitialState({ + selectedProviderId: null +}); - return { +export function reducer(state = initialState, action: provider.ProviderCollectionActionsUnion): ProviderCollectionState { + switch (action.type) { + case ProviderCollectionActionTypes.LOAD_PROVIDER_SUCCESS: { + return adapter.addMany(action.payload, { ...state, - ids: [...providerIds], - entities: Object.assign(entities) - }; + selectedProviderId: state.selectedProviderId + }); } - case provider.SELECT_SUCCESS: { - const provider = action.payload; - - if (state.ids.indexOf(provider.id) < 0) { - return state; - } - return { - ids: [...state.ids.filter(id => id !== provider.id), provider.id], - entities: Object.assign({ ...state.entities }, { - [provider.id]: provider, - }), - selectedProviderId: provider.id - }; - } - - case provider.UPDATE_PROVIDER_SUCCESS: { - const provider = action.payload; - - if (state.ids.indexOf(provider.id) < 0) { - return state; - } - const original = state.entities[provider.id], - updated = Object.assign({}, - { ...original }, - { ...provider } - ); - return { - ...state, - ids: [...state.ids.filter(id => id !== provider.id), provider.id], - entities: Object.assign({ ...state.entities }, { - [provider.id]: provider, - }) - }; + case ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS: { + return adapter.updateOne(action.payload, state); } - case provider.SELECT: { + case ProviderCollectionActionTypes.SELECT: { return { ...state, selectedProviderId: action.payload, @@ -84,16 +48,10 @@ export function reducer(state = initialState, action: provider.Actions): Provide } } -export const getEntities = (state: ProviderCollectionState) => state.entities; -export const getIds = (state: ProviderCollectionState) => state.ids; -export const getSelectedId = (state: ProviderCollectionState) => state.selectedProviderId; -export const getSelected = createSelector( - getEntities, - getSelectedId, - (entities, selectedId) => { - return entities[selectedId]; - } -); -export const getAll = createSelector(getEntities, getIds, (entities, ids) => { - return ids.map(id => entities[id]).filter(entity => entity); -}); +export const getSelectedProviderId = (state: ProviderCollectionState) => state.selectedProviderId; +export const { + selectIds: selectProviderIds, + selectEntities: selectProviderEntities, + selectAll: selectAllProviders, + selectTotal: selectProviderTotal +} = adapter.getSelectors(); diff --git a/ui/src/app/domain/service/entity-descriptor.service.ts b/ui/src/app/domain/service/entity-descriptor.service.ts index 0e15e4ee4..ec0b38b46 100644 --- a/ui/src/app/domain/service/entity-descriptor.service.ts +++ b/ui/src/app/domain/service/entity-descriptor.service.ts @@ -7,6 +7,7 @@ import { MetadataProvider } from '../../domain/model/metadata-provider'; import { MOCK_DESCRIPTORS } from '../../../data/descriptors.mock'; import { Storage } from '../../shared/storage'; import { environment } from '../../../environments/environment'; +import { MetadataEntity } from '../domain.type'; @Injectable() export class EntityDescriptorService { @@ -63,8 +64,8 @@ export class EntityDescriptorService { }); } - preview(provider: MetadataProvider): Observable { - return this.http.get(`${this.base}${this.endpoint}/${provider.id}`, { + preview(id: string): Observable { + return this.http.get(`${this.base}${this.endpoint}/${id}`, { headers: new HttpHeaders({ 'Accept': 'application/xml' }), diff --git a/ui/src/app/domain/service/entity-id.service.ts b/ui/src/app/domain/service/entity-id.service.ts index 5fb3049bf..6bc3125d5 100644 --- a/ui/src/app/domain/service/entity-id.service.ts +++ b/ui/src/app/domain/service/entity-id.service.ts @@ -8,13 +8,16 @@ import { IDS } from '../../../data/ids.mock'; import { Storage } from '../../shared/storage'; import { environment } from '../../../environments/environment'; import { QueryParams } from '../../core/model/query'; +import { MDUI } from '../model/mdui'; +import * as XmlFormatter from 'xml-formatter'; const MOCK_INTERVAL = 500; @Injectable() export class EntityIdService { - private endpoint = '/EntityIds/search'; + private searchEndpoint = '/EntityIds/search'; + private entitiesEndpoint = '/entities'; private base = '/api'; private subj: Subject = new Subject(); @@ -28,11 +31,27 @@ export class EntityIdService { Object.keys(q).forEach(key => params = params.set(key, q[key])); const opts = { params: params }; return this.http - .get(`${this.base}${this.endpoint}`, opts) + .get(`${this.base}${this.searchEndpoint}`, opts) .map(resp => resp.entityIds) .catch(err => { console.log('ERROR LOADING IDS:', err); return Observable.of([] as string[]); }); } + + preview(id: string): Observable { + return this.http + .get(`${this.base}${this.entitiesEndpoint}/${encodeURIComponent(id)}`, { + headers: new HttpHeaders({ + 'Accept': 'application/xml' + }), + responseType: 'text' + }); + } + + findEntityById(id: string): Observable { + return this.http + .get(`${this.base}${this.entitiesEndpoint}/${encodeURIComponent(id)}`) + .map(entity => entity.mdui as MDUI); + } } diff --git a/ui/src/app/domain/service/metadata-resolver.service.ts b/ui/src/app/domain/service/metadata-resolver.service.ts index d13542963..447e306f7 100644 --- a/ui/src/app/domain/service/metadata-resolver.service.ts +++ b/ui/src/app/domain/service/metadata-resolver.service.ts @@ -46,50 +46,26 @@ export class MetadataResolverService { private http: HttpClient ) {} query(): Observable { - /* return this.http .get(`${this.base}${this.endpoint}s`) .catch(err => { console.log('ERROR LOADING PROVIDERS:', err); return Observable.of([] as MetadataFilter[]); }); - */ - return of(this.filters); } find(id: string): Observable { - /* return this .http .get(`${this.base}${this.endpoint}/${id}`) .catch(err => Observable.throw(err)); - */ - let filter = this.filters.find(f => f.id === id); - if (!filter) { - return Observable.throw('Not Found!'); - } - return of(filter); } update(filter: MetadataFilter): Observable { - /* - return this.http.put(`${this.base}${this.endpoint}/${provider.id}`, provider); - */ - let original = this.filters.find(f => f.id === filter.id); - Object.assign(original, filter); - return of(original); + return this.http.put(`${this.base}${this.endpoint}/${filter.id}`, filter); } save(filter: MetadataFilter): Observable { - // return this.http.post(`${this.base}${this.endpoint}`, provider); - const saved = { - ...filter, - id: `${filter.filterName}-${Date.now()}`, - createdDate: '2018-04-05T09:07:13.730', - modifiedDate: '2018-04-05T09:07:13.730' - }; - console.log(saved); - this.filters.push(saved); - return Observable.of(saved); + return this.http.post(`${this.base}${this.endpoint}`, filter); } } diff --git a/ui/src/app/edit-provider/container/wizard.component.ts b/ui/src/app/edit-provider/container/wizard.component.ts index b4e66bea1..28140cc0f 100644 --- a/ui/src/app/edit-provider/container/wizard.component.ts +++ b/ui/src/app/edit-provider/container/wizard.component.ts @@ -113,7 +113,7 @@ export class WizardComponent implements OnInit, OnDestroy, CanComponentDeactivat this.changes$ .takeUntil(this.ngUnsubscribe) .skipWhile(() => this.saving) - .combineLatest(this.provider$, (changes, base) => Object.assign({}, base, changes)) + .combineLatest(this.provider$, (changes, base) => ({ ...base, ...changes })) .subscribe(latest => this.latest = latest); this.valueEmitter diff --git a/ui/src/app/edit-provider/effect/editor.effect.ts b/ui/src/app/edit-provider/effect/editor.effect.ts index 019ce1634..77c26c127 100644 --- a/ui/src/app/edit-provider/effect/editor.effect.ts +++ b/ui/src/app/edit-provider/effect/editor.effect.ts @@ -3,6 +3,7 @@ import { Effect, Actions } from '@ngrx/effects'; import * as editor from '../action/editor.action'; import * as provider from '../../domain/action/provider-collection.action'; +import { ProviderCollectionActionTypes } from '../../domain/action/provider-collection.action'; import { MetadataProvider } from '../../domain/model/metadata-provider'; import { EntityDescriptorService } from '../../domain/service/entity-descriptor.service'; import { Router } from '@angular/router'; @@ -17,7 +18,7 @@ export class EditorEffects { @Effect() updateProviderSuccessRedirect$ = this.actions$ - .ofType(provider.UPDATE_PROVIDER_SUCCESS) + .ofType(ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS) .map(action => action.payload) .map(p => new editor.ResetChanges()); diff --git a/ui/src/app/edit-provider/effect/wizard.effect.ts b/ui/src/app/edit-provider/effect/wizard.effect.ts index 95daca49d..4ceb4134f 100644 --- a/ui/src/app/edit-provider/effect/wizard.effect.ts +++ b/ui/src/app/edit-provider/effect/wizard.effect.ts @@ -4,6 +4,7 @@ import { Effect, Actions } from '@ngrx/effects'; import * as editorActions from '../action/editor.action'; import * as draft from '../../domain/action/draft-collection.action'; import * as provider from '../../domain/action/provider-collection.action'; +import { ProviderCollectionActionTypes } from '../../domain/action/provider-collection.action'; import { MetadataProvider } from '../../domain/model/metadata-provider'; import { EntityDraftService } from '../../domain/service/entity-draft.service'; @@ -19,7 +20,7 @@ export class WizardEffects { }); @Effect() addProviderSuccessDiscard$ = this.actions$ - .ofType(provider.ADD_PROVIDER_SUCCESS) + .ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS) .map(action => action.payload) .map(provider => { return new editorActions.ResetChanges(); diff --git a/ui/src/app/edit-provider/reducer/editor.reducer.ts b/ui/src/app/edit-provider/reducer/editor.reducer.ts index db1588c54..1513c1403 100644 --- a/ui/src/app/edit-provider/reducer/editor.reducer.ts +++ b/ui/src/app/edit-provider/reducer/editor.reducer.ts @@ -2,6 +2,7 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; import { MetadataProvider } from '../../domain/model/metadata-provider'; import * as editor from '../action/editor.action'; import * as provider from '../../domain/action/provider-collection.action'; +import { ProviderCollectionActionTypes, ProviderCollectionActionsUnion } from '../../domain/action/provider-collection.action'; import * as fromRoot from '../../core/reducer'; export interface EditorState { @@ -16,21 +17,21 @@ export const initialState: EditorState = { changes: {} as MetadataProvider }; -export function reducer(state = initialState, action: editor.Actions | provider.Actions): EditorState { +export function reducer(state = initialState, action: editor.Actions | ProviderCollectionActionsUnion): EditorState { switch (action.type) { - case provider.ADD_PROVIDER: { + case ProviderCollectionActionTypes.ADD_PROVIDER: { return { ...state, saving: true, }; } - case provider.ADD_PROVIDER_FAIL: { + case ProviderCollectionActionTypes.ADD_PROVIDER_FAIL: { return { ...state, saving: false }; } - case provider.ADD_PROVIDER_SUCCESS: { + case ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS: { return { ...state, changes: { ...initialState.changes }, diff --git a/ui/src/app/metadata-filter/action/filter.action.ts b/ui/src/app/metadata-filter/action/filter.action.ts index a75ddae64..93e00d64a 100644 --- a/ui/src/app/metadata-filter/action/filter.action.ts +++ b/ui/src/app/metadata-filter/action/filter.action.ts @@ -2,6 +2,7 @@ import { Action } from '@ngrx/store'; import { QueryParams } from '../../core/model/query'; import { MetadataFilter } from '../../domain/model/metadata-filter'; +import { MDUI } from '../../domain/model/mdui'; export const SELECT_ID = '[Filter] Select Entity ID'; @@ -9,12 +10,32 @@ export const CREATE_FILTER = '[Filter] Create Filter'; export const UPDATE_FILTER = '[Filter] Update Filter'; export const CANCEL_CREATE_FILTER = '[Filter] Cancel Create Filter'; +export const LOAD_ENTITY_PREVIEW = '[Filter] Load Preview data'; +export const LOAD_ENTITY_PREVIEW_SUCCESS = '[Filter] Load Preview data success'; +export const LOAD_ENTITY_PREVIEW_ERROR = '[Filter] Load Preview data error'; + export class SelectId implements Action { readonly type = SELECT_ID; constructor(public payload: string) { } } +export class LoadEntityPreview implements Action { + readonly type = LOAD_ENTITY_PREVIEW; + + constructor(public payload: string) { } +} +export class LoadEntityPreviewSuccess implements Action { + readonly type = LOAD_ENTITY_PREVIEW_SUCCESS; + + constructor(public payload: MDUI) { } +} +export class LoadEntityPreviewError implements Action { + readonly type = LOAD_ENTITY_PREVIEW_ERROR; + + constructor(public payload: string) { } +} + export class CreateFilter implements Action { readonly type = CREATE_FILTER; @@ -35,4 +56,7 @@ export type Actions = | SelectId | CreateFilter | UpdateFilterChanges - | CancelCreateFilter; + | CancelCreateFilter + | LoadEntityPreview + | LoadEntityPreviewSuccess + | LoadEntityPreviewError; diff --git a/ui/src/app/metadata-filter/action/search.action.ts b/ui/src/app/metadata-filter/action/search.action.ts index 75f7da0c9..a9312eff3 100644 --- a/ui/src/app/metadata-filter/action/search.action.ts +++ b/ui/src/app/metadata-filter/action/search.action.ts @@ -5,6 +5,7 @@ import { QueryParams } from '../../core/model/query'; export const QUERY_ENTITY_IDS = '[Filter] Query Entity Ids'; export const VIEW_MORE_IDS = '[Filter] View More Ids Modal'; export const CANCEL_VIEW_MORE = '[Filter] Cancel View More'; +export const CLEAR_SEARCH = '[Filter] Clear Search'; export const LOAD_ENTITY_IDS_SUCCESS = '[Entity ID Collection] Load Entity Ids Success'; export const LOAD_ENTITY_IDS_ERROR = '[Entity ID Collection] Load Entity Ids Error'; @@ -20,6 +21,10 @@ export class ViewMoreIds implements Action { constructor(public payload: string) { } } +export class ClearSearch implements Action { + readonly type = CLEAR_SEARCH; +} + export class CancelViewMore implements Action { readonly type = CANCEL_VIEW_MORE; } @@ -39,6 +44,7 @@ export class LoadEntityIdsError implements Action { export type Actions = | ViewMoreIds | CancelViewMore + | ClearSearch | LoadEntityIdsSuccess | LoadEntityIdsError | QueryEntityIds; diff --git a/ui/src/app/metadata-filter/component/preview-filter.component.html b/ui/src/app/metadata-filter/component/preview-filter.component.html deleted file mode 100644 index ecf5de875..000000000 --- a/ui/src/app/metadata-filter/component/preview-filter.component.html +++ /dev/null @@ -1,14 +0,0 @@ - - - \ No newline at end of file diff --git a/ui/src/app/metadata-filter/component/preview-filter.component.ts b/ui/src/app/metadata-filter/component/preview-filter.component.ts deleted file mode 100644 index 06c654da3..000000000 --- a/ui/src/app/metadata-filter/component/preview-filter.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, AfterViewInit, Input, OnInit, SimpleChange, SimpleChanges } from '@angular/core'; -import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { Observable } from 'rxjs/Observable'; -import { Store } from '@ngrx/store'; - -import * as fromFilter from '../reducer'; -import { QueryEntityIds } from '../action/search.action'; -import { FormBuilder, FormGroup } from '@angular/forms'; -import { MetadataFilter } from '../../domain/domain.type'; - -@Component({ - selector: 'preview-filter', - templateUrl: './preview-filter.component.html' -}) -export class PreviewFilterComponent { - filter$: Observable; - constructor( - public activeModal: NgbActiveModal, - private store: Store - ) { - this.filter$ = this.store.select(fromFilter.getFilter); - } -} diff --git a/ui/src/app/metadata-filter/container/edit-filter.component.html b/ui/src/app/metadata-filter/container/edit-filter.component.html index 189e235e1..187420e70 100644 --- a/ui/src/app/metadata-filter/container/edit-filter.component.html +++ b/ui/src/app/metadata-filter/container/edit-filter.component.html @@ -95,7 +95,7 @@
- @@ -110,25 +110,27 @@

-
+
-
- +
+
Display Name
+
{{ (preview$ | async).displayName }}
+
Description
+
{{ (preview$ | async).description || '—' }}
+
+
diff --git a/ui/src/app/metadata-filter/container/edit-filter.component.ts b/ui/src/app/metadata-filter/container/edit-filter.component.ts index 6cc96f193..8a7e9f0cf 100644 --- a/ui/src/app/metadata-filter/container/edit-filter.component.ts +++ b/ui/src/app/metadata-filter/container/edit-filter.component.ts @@ -18,8 +18,11 @@ import { MetadataFilter } from '../../domain/model/metadata-filter'; import { Filter } from '../../domain/entity/filter'; import { EntityValidators } from '../../domain/service/entity-validators.service'; import { SearchDialogComponent } from '../component/search-dialog.component'; -import { QueryEntityIds, ViewMoreIds } from '../action/search.action'; +import { QueryEntityIds, ViewMoreIds, ClearSearch } from '../action/search.action'; import { AutoCompleteComponent } from '../../shared/autocomplete/autocomplete.component'; +import { MDUI } from '../../domain/model/mdui'; +import { PreviewEntity } from '../../domain/action/entity.action'; +import { MetadataEntity } from '../../domain/domain.type'; @Component({ selector: 'edit-filter-page', @@ -43,6 +46,7 @@ export class EditFilterComponent implements OnInit, OnDestroy { filter$: Observable; loading$: Observable; processing$: Observable; + preview$: Observable; form: FormGroup = this.fb.group({ entityId: ['', [Validators.required]], @@ -60,7 +64,9 @@ export class EditFilterComponent implements OnInit, OnDestroy { private fb: FormBuilder ) { this.changes$ = this.store.select(fromFilter.getFilter); - this.changes$.subscribe(c => this.changes = c); + this.changes$.subscribe(c => { + this.changes = new Filter(c); + }); this.showMore$ = this.store.select(fromFilter.getViewingMore); this.selected$ = this.store.select(fromFilter.getSelected); @@ -68,21 +74,26 @@ export class EditFilterComponent implements OnInit, OnDestroy { this.entityIds$ = this.store.select(fromFilter.getEntityCollection); this.loading$ = this.store.select(fromFilter.getIsLoading); this.processing$ = this.loading$.withLatestFrom(this.showMore$, (l, s) => !s && l); + this.preview$ = this.store.select(fromFilter.getPreview); this.entityIds$.subscribe(ids => this.ids = ids); this.filter$.subscribe(filter => { - let { entityId, filterName, filterEnabled } = filter; + let { entityId, filterName, filterEnabled } = new Filter(filter); this.form.reset({ entityId, filterName, filterEnabled }); this.filter = filter; + + this.store.dispatch(new SelectId(entityId)); }); } ngOnInit(): void { + this.store.dispatch(new ClearSearch()); + let id = this.form.get('entityId'); id.valueChanges .distinctUntilChanged() @@ -100,6 +111,15 @@ export class EditFilterComponent implements OnInit, OnDestroy { .subscribe(changes => this.store.dispatch(new UpdateFilterChanges(changes))); this.form.get('entityId').disable(); + + id + .valueChanges + .distinctUntilChanged() + .subscribe(entityId => { + if (id.valid) { + this.store.dispatch(new SelectId(entityId)); + } + }); } ngOnDestroy(): void { @@ -123,7 +143,7 @@ export class EditFilterComponent implements OnInit, OnDestroy { } searchEntityIds(term: string): void { - if (term.length >= 4 && this.ids.indexOf(term) < 0) { + if (term && term.length >= 4 && this.ids.indexOf(term) < 0) { this.store.dispatch(new QueryEntityIds({ term, limit: 10 @@ -140,14 +160,14 @@ export class EditFilterComponent implements OnInit, OnDestroy { } save(): void { - this.store.dispatch(new UpdateFilterRequest({...this.filter, ...this.changes})); + this.store.dispatch(new UpdateFilterRequest({...this.filter, ...this.changes.serialize()})); } cancel(): void { this.store.dispatch(new CancelCreateFilter()); } - preview(): void { - console.log('preview XML'); + preview(entity: MetadataEntity): void { + this.store.dispatch(new PreviewEntity(new Filter(entity))); } } diff --git a/ui/src/app/metadata-filter/container/new-filter.component.html b/ui/src/app/metadata-filter/container/new-filter.component.html index 34599235d..dec981eaf 100644 --- a/ui/src/app/metadata-filter/container/new-filter.component.html +++ b/ui/src/app/metadata-filter/container/new-filter.component.html @@ -81,25 +81,27 @@

-
+
-
- +
+
Display Name
+
{{ (preview$ | async).displayName }}
+
Description
+
{{ (preview$ | async).description || '—' }}
+
+
diff --git a/ui/src/app/metadata-filter/container/new-filter.component.ts b/ui/src/app/metadata-filter/container/new-filter.component.ts index 3717fabf1..5b67a08b9 100644 --- a/ui/src/app/metadata-filter/container/new-filter.component.ts +++ b/ui/src/app/metadata-filter/container/new-filter.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { Component, OnInit, OnDestroy, SimpleChanges } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; @@ -17,17 +17,16 @@ import { MetadataFilter } from '../../domain/model/metadata-filter'; import { Filter } from '../../domain/entity/filter'; import { EntityValidators } from '../../domain/service/entity-validators.service'; import { SearchDialogComponent } from '../component/search-dialog.component'; -import { QueryEntityIds, ViewMoreIds } from '../action/search.action'; +import { QueryEntityIds, ViewMoreIds, ClearSearch } from '../action/search.action'; +import { MDUI } from '../../domain/model/mdui'; @Component({ selector: 'new-filter-page', templateUrl: './new-filter.component.html' }) -export class NewFilterComponent implements OnInit, OnChanges, OnDestroy { +export class NewFilterComponent implements OnInit, OnDestroy { private ngUnsubscribe: Subject = new Subject(); - private valueEmitSubscription: Subscription; - private statusEmitSubscription: Subscription; changes$: Observable; changes: MetadataFilter; @@ -54,6 +53,7 @@ export class NewFilterComponent implements OnInit, OnChanges, OnDestroy { selected$: Observable; loading$: Observable; processing$: Observable; + preview$: Observable; form: FormGroup = this.fb.group({ entityId: ['', [Validators.required], [ @@ -71,32 +71,27 @@ export class NewFilterComponent implements OnInit, OnChanges, OnDestroy { private fb: FormBuilder ) { this.changes$ = this.store.select(fromFilter.getFilter); - this.changes$.subscribe(c => this.changes = c); + this.changes$.subscribe(c => this.changes = new Filter(c)); this.showMore$ = this.store.select(fromFilter.getViewingMore); this.selected$ = this.store.select(fromFilter.getSelected); this.entityIds$ = this.store.select(fromFilter.getEntityCollection); this.loading$ = this.store.select(fromFilter.getIsLoading); this.processing$ = this.loading$.withLatestFrom(this.showMore$, (l, s) => !s && l); + this.preview$ = this.store.select(fromFilter.getPreview); this.entityIds$.subscribe(ids => this.ids = ids); - - this.selected$.subscribe(s => { - this.form.patchValue({ - entityId: s - }); - }); } ngOnInit(): void { - this.store.dispatch(new CreateFilter(this.filter)); + this.store.dispatch(new ClearSearch()); let id = this.form.get('entityId'); id.valueChanges .distinctUntilChanged() .subscribe(query => this.searchEntityIds(query)); - this.valueEmitSubscription = this.form + this.form .valueChanges .takeUntil(this.ngUnsubscribe) .startWith(this.form.value) @@ -112,22 +107,16 @@ export class NewFilterComponent implements OnInit, OnChanges, OnDestroy { .takeUntil(this.ngUnsubscribe) .startWith(this.form.status) .subscribe(status => this.onStatusChange.emit(status));*/ - } - ngOnChanges(changes: SimpleChanges): void { - if (changes.entity) { - let { entityId, filterName, filterEnabled } = this.filter; - this.form.reset({ - entityId, - filterName, - filterEnabled + + id + .valueChanges + .distinctUntilChanged() + .subscribe(entityId => { + if (id.valid) { + this.store.dispatch(new SelectId(entityId)); + } }); - this.form.updateValueAndValidity(); - if (changes.entity.firstChange && this.filter.entityId) { - this.store.dispatch(new SelectId(this.filter.entityId)); - this.searchEntityIds(this.filter.entityId); - } - } } ngOnDestroy(): void { @@ -153,7 +142,7 @@ export class NewFilterComponent implements OnInit, OnChanges, OnDestroy { } save(): void { - this.store.dispatch(new AddFilterRequest(this.changes)); + this.store.dispatch(new AddFilterRequest(this.changes.serialize())); } cancel(): void { diff --git a/ui/src/app/metadata-filter/effect/filter.effect.ts b/ui/src/app/metadata-filter/effect/filter.effect.ts index d860fe7b9..b5eff6ad8 100644 --- a/ui/src/app/metadata-filter/effect/filter.effect.ts +++ b/ui/src/app/metadata-filter/effect/filter.effect.ts @@ -10,6 +10,7 @@ import 'rxjs/add/operator/switchMap'; import * as filter from '../action/filter.action'; import * as fromFilter from '../reducer'; import * as collection from '../../domain/action/filter-collection.action'; +import { FilterCollectionActionTypes } from '../../domain/action/filter-collection.action'; import { SearchDialogComponent } from '../component/search-dialog.component'; import { EntityIdService } from '../../domain/service/entity-id.service'; @@ -17,9 +18,21 @@ import { MetadataResolverService } from '../../domain/service/metadata-resolver. @Injectable() export class FilterEffects { + + @Effect() + loadEntityMdui$ = this.actions$ + .ofType(filter.SELECT_ID) + .map(action => action.payload) + .switchMap(query => + this.idService + .findEntityById(query) + .map(data => new filter.LoadEntityPreviewSuccess(data)) + .catch(error => Observable.of(new filter.LoadEntityPreviewError(error))) + ); + @Effect({ dispatch: false }) saveFilterSuccess$ = this.actions$ - .ofType(collection.ADD_FILTER_SUCCESS) + .ofType(FilterCollectionActionTypes.ADD_FILTER_SUCCESS) .switchMap(() => this.router.navigate(['/dashboard'])); @Effect({ dispatch: false }) diff --git a/ui/src/app/metadata-filter/filter.module.ts b/ui/src/app/metadata-filter/filter.module.ts index 8c30c4d43..61bf73a8a 100644 --- a/ui/src/app/metadata-filter/filter.module.ts +++ b/ui/src/app/metadata-filter/filter.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; import { ReactiveFormsModule } from '@angular/forms'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; @@ -13,11 +14,12 @@ import { FilterEffects } from './effect/filter.effect'; import { NgbPopoverModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; import { SearchDialogComponent } from './component/search-dialog.component'; import { SharedModule } from '../shared/shared.module'; -import { PreviewFilterComponent } from './component/preview-filter.component'; import { EditFilterComponent } from './container/edit-filter.component'; import { FilterComponent } from './container/filter.component'; import { SearchIdEffects } from './effect/search.effect'; import { FilterExistsGuard } from '../domain/guard/filter-exists.guard'; +import { PreviewDialogModule } from '../shared/preview/preview-dialog.module'; + export const routes: Routes = [ { @@ -44,24 +46,23 @@ export const routes: Routes = [ NewFilterComponent, EditFilterComponent, FilterComponent, - SearchDialogComponent, - PreviewFilterComponent + SearchDialogComponent ], entryComponents: [ - SearchDialogComponent, - PreviewFilterComponent + SearchDialogComponent ], imports: [ - CommonModule, - RouterModule, - ReactiveFormsModule, + RouterModule.forChild(routes), StoreModule.forFeature('metadata-filter', reducers), EffectsModule.forFeature([FilterEffects, SearchIdEffects]), - RouterModule.forChild(routes), + CommonModule, + ReactiveFormsModule, ProviderEditorFormModule, NgbPopoverModule, NgbModalModule, - SharedModule + SharedModule, + PreviewDialogModule, + HttpClientModule ], providers: [ FilterExistsGuard diff --git a/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts b/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts index ca2c2f9e9..f482928c5 100644 --- a/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts +++ b/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts @@ -4,7 +4,8 @@ import * as actions from '../action/filter.action'; const snapshot: fromFilter.FilterState = { selected: null, - changes: null + changes: null, + preview: null }; describe('Filter Reducer', () => { diff --git a/ui/src/app/metadata-filter/reducer/filter.reducer.ts b/ui/src/app/metadata-filter/reducer/filter.reducer.ts index 1092dd684..86f2e04b7 100644 --- a/ui/src/app/metadata-filter/reducer/filter.reducer.ts +++ b/ui/src/app/metadata-filter/reducer/filter.reducer.ts @@ -1,20 +1,23 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; import * as filter from '../action/filter.action'; import * as collection from '../../domain/action/filter-collection.action'; +import { FilterCollectionActionTypes, FilterCollectionActionsUnion } from '../../domain/action/filter-collection.action'; import * as fromRoot from '../../core/reducer'; -import { MetadataFilter } from '../../domain/domain.type'; +import { MetadataFilter, MDUI } from '../../domain/domain.type'; export interface FilterState { selected: string | null; changes: MetadataFilter | null; + preview: MDUI | null; } export const initialState: FilterState = { selected: null, - changes: null + changes: null, + preview: null }; -export function reducer(state = initialState, action: filter.Actions | collection.Actions): FilterState { +export function reducer(state = initialState, action: filter.Actions | FilterCollectionActionsUnion): FilterState { switch (action.type) { case filter.SELECT_ID: { return { @@ -22,6 +25,12 @@ export function reducer(state = initialState, action: filter.Actions | collectio selected: action.payload }; } + case filter.LOAD_ENTITY_PREVIEW_SUCCESS: { + return { + ...state, + preview: action.payload + }; + } case filter.CREATE_FILTER: { return { ...state, @@ -37,8 +46,8 @@ export function reducer(state = initialState, action: filter.Actions | collectio } }; } - case collection.ADD_FILTER_SUCCESS: - case collection.UPDATE_FILTER_SUCCESS: + case FilterCollectionActionTypes.ADD_FILTER_SUCCESS: + case FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS: case filter.CANCEL_CREATE_FILTER: { return { ...initialState @@ -52,3 +61,4 @@ export function reducer(state = initialState, action: filter.Actions | collectio export const getSelected = (state: FilterState) => state.selected; export const getFilterChanges = (state: FilterState) => state.changes; +export const getPreview = (state: FilterState) => state.preview; diff --git a/ui/src/app/metadata-filter/reducer/index.ts b/ui/src/app/metadata-filter/reducer/index.ts index 49b75b532..73b342ff2 100644 --- a/ui/src/app/metadata-filter/reducer/index.ts +++ b/ui/src/app/metadata-filter/reducer/index.ts @@ -24,6 +24,7 @@ export const getFilterState = createFeatureSelector('metadata-filte export const getFilterFromState = createSelector(getFilterState, getFiltersFromStateFn); export const getSelected = createSelector(getFilterFromState, fromFilter.getSelected); export const getFilter = createSelector(getFilterFromState, fromFilter.getFilterChanges); +export const getPreview = createSelector(getFilterFromState, fromFilter.getPreview); export const getSearchFromState = createSelector(getFilterState, getSearchFromStateFn); export const getEntityCollection = createSelector(getSearchFromState, fromSearch.getEntityIds); diff --git a/ui/src/app/metadata-filter/reducer/search.reducer.ts b/ui/src/app/metadata-filter/reducer/search.reducer.ts index 0d7374171..42537d185 100644 --- a/ui/src/app/metadata-filter/reducer/search.reducer.ts +++ b/ui/src/app/metadata-filter/reducer/search.reducer.ts @@ -1,7 +1,9 @@ import { createSelector, createFeatureSelector } from '@ngrx/store'; import * as search from '../action/search.action'; +import * as filter from '../action/filter.action'; import * as fromRoot from '../../core/reducer'; import { MetadataFilter } from '../../domain/domain.type'; +import { FilterCollectionActionTypes, FilterCollectionActionsUnion } from '../../domain/action/filter-collection.action'; export interface SearchState { entityIds: string[]; @@ -19,7 +21,7 @@ export const initialState: SearchState = { term: '', }; -export function reducer(state = initialState, action: search.Actions): SearchState { +export function reducer(state = initialState, action: search.Actions | filter.Actions | FilterCollectionActionsUnion): SearchState { switch (action.type) { case search.VIEW_MORE_IDS: { return { @@ -55,6 +57,14 @@ export function reducer(state = initialState, action: search.Actions): SearchSta error: action.payload }; } + case FilterCollectionActionTypes.ADD_FILTER_SUCCESS: + case FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS: + case search.CLEAR_SEARCH: + case filter.CANCEL_CREATE_FILTER: { + return { + ...initialState + }; + } default: { return state; } diff --git a/ui/src/app/metadata-provider/metadata-provider.module.ts b/ui/src/app/metadata-provider/metadata-provider.module.ts index 143b67ba1..13f27a75a 100644 --- a/ui/src/app/metadata-provider/metadata-provider.module.ts +++ b/ui/src/app/metadata-provider/metadata-provider.module.ts @@ -9,8 +9,7 @@ import { EffectsModule } from '@ngrx/effects'; import { NewProviderComponent } from './container/new-provider.component'; import { ProviderEditorFormModule } from './component'; -import { PreviewProviderDialogComponent } from './component/preview-provider-dialog.component'; -import { PretttyXml } from './pipe/pretty-xml.pipe'; +import { PrettyXml } from './pipe/pretty-xml.pipe'; import { UploadProviderComponent } from './container/upload-provider.component'; import { BlankProviderComponent } from './container/blank-provider.component'; @@ -20,12 +19,9 @@ import { BlankProviderComponent } from './container/blank-provider.component'; NewProviderComponent, UploadProviderComponent, BlankProviderComponent, - PreviewProviderDialogComponent, - PretttyXml, - ], - entryComponents: [ - PreviewProviderDialogComponent + PrettyXml, ], + entryComponents: [], imports: [ HttpClientModule, CommonModule, diff --git a/ui/src/app/metadata-provider/pipe/pretty-xml.pipe.ts b/ui/src/app/metadata-provider/pipe/pretty-xml.pipe.ts index d46a81830..d0fc54d05 100644 --- a/ui/src/app/metadata-provider/pipe/pretty-xml.pipe.ts +++ b/ui/src/app/metadata-provider/pipe/pretty-xml.pipe.ts @@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import * as XmlFormatter from 'xml-formatter'; @Pipe({ name: 'prettyXml' }) -export class PretttyXml implements PipeTransform { +export class PrettyXml implements PipeTransform { transform(value: string): string { if (!value) { return value; diff --git a/ui/src/app/metadata-provider/component/preview-provider-dialog.component.html b/ui/src/app/shared/preview/preview-dialog.component.html similarity index 86% rename from ui/src/app/metadata-provider/component/preview-provider-dialog.component.html rename to ui/src/app/shared/preview/preview-dialog.component.html index 8b4645ac2..20c1f49c7 100644 --- a/ui/src/app/metadata-provider/component/preview-provider-dialog.component.html +++ b/ui/src/app/shared/preview/preview-dialog.component.html @@ -1,11 +1,11 @@