diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index b60c9b0c8..f63dbad31 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -43,6 +43,9 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private OpenSamlObjects openSamlObjects + @Autowired + private MetadataResolversPositionOrderContainerService resolversPositionOrderContainerService + // TODO: enhance @Override void reloadFilters(String metadataResolverName) { @@ -98,8 +101,11 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { 'xsi:type': 'ChainingMetadataProvider', 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' ) { - metadataResolverRepository.findAll().each { edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - //TODO: We cannot/do not currently have the code to marshall the internal incommon chaining resolver + + + resolversPositionOrderContainerService.allMetadataResolversInDefinedOrderOrUnordered.each { + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + //TODO: We do not currently marshall the internal incommon chaining resolver (with BaseMetadataResolver type) if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { constructXmlNodeForResolver(mr, delegate) { MetadataFilter( 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 ca7be77ea..c630752f8 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -1,8 +1,11 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityIdsSearchResultRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks; import edu.internet2.tier.shibboleth.admin.ui.service.*; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; @@ -162,4 +165,14 @@ public void addInterceptors(InterceptorRegistry registry) { } }; } + + @Bean + public MetadataResolversPositionOrderContainerService + metadataResolversPositionOrderContainerService(MetadataResolversPositionOrderContainerRepository + positionOrderContainerRepository, + MetadataResolverRepository resolverRepository) { + + return new DefaultMetadataResolversPositionOrderContainerService(positionOrderContainerRepository, resolverRepository); + + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 9705ddacc..2b1ddc6b5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -5,6 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -30,6 +31,7 @@ import java.io.IOException; import java.io.StringWriter; import java.net.URI; +import java.util.List; import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; @@ -47,6 +49,9 @@ public class MetadataResolversController { @Autowired MetadataResolverService metadataResolverService; + @Autowired + MetadataResolversPositionOrderContainerService positionOrderContainerService; + @ExceptionHandler({InvalidTypeIdException.class, IOException.class, HttpMessageNotReadableException.class}) public ResponseEntity unableToParseJson(Exception ex) { return ResponseEntity.badRequest().body(new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), ex.getMessage())); @@ -55,7 +60,7 @@ public ResponseEntity unableToParseJson(Exception ex) { @GetMapping("/MetadataResolvers") @Transactional(readOnly = true) public ResponseEntity getAll() { - Iterable resolvers = resolverRepository.findAll(); + List resolvers = positionOrderContainerService.getAllMetadataResolversInDefinedOrderOrUnordered(); resolvers.forEach(MetadataResolver::updateVersion); return ResponseEntity.ok(resolvers); } @@ -99,6 +104,7 @@ public ResponseEntity create(@RequestBody MetadataResolver newResolver) { newResolver.convertFiltersFromTransientRepresentationIfNecessary(); MetadataResolver persistedResolver = resolverRepository.save(newResolver); + positionOrderContainerService.appendPositionOrderForNew(persistedResolver); persistedResolver.updateVersion(); persistedResolver.convertFiltersIntoTransientRepresentationIfNecessary(); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversPositionOrderController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversPositionOrderController.java new file mode 100644 index 000000000..ab78e4a52 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversPositionOrderController.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Dmitriy Kopylenko + */ +@RestController +@RequestMapping("/api/MetadataResolversPositionOrder") +public class MetadataResolversPositionOrderController { + + @Autowired + MetadataResolversPositionOrderContainerService positionOrderContainerService; + + @PostMapping + public ResponseEntity createOrUpdate(@RequestBody MetadataResolversPositionOrderContainer metadataResolversPositionOrderContainer) { + positionOrderContainerService.addOrUpdatePositionOrderContainer(metadataResolversPositionOrderContainer); + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity getPositionOrderContainer() { + return ResponseEntity.ok(positionOrderContainerService.retrieveExistingOrEmpty()); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversOrderContainer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversOrderContainer.java new file mode 100644 index 000000000..5b5d0d87f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversOrderContainer.java @@ -0,0 +1,14 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +/** + * This is a persistent entity abstraction encapsulating a collection of metadata resolver ids + * for the purpose of maintaining an order of all persistent metadata resolvers which becomes significant during + * generation of XML metadata for the resolvers. + * + * Maintaining this separate entity enables UI layer for example to explicitly manipulate ordering e.g. use REST + * API to reorder resolvers, etc. + * + * @author Dmitriy + */ +public class MetadataResolversOrderContainer { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversPositionOrderContainer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversPositionOrderContainer.java new file mode 100644 index 000000000..883069531 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolversPositionOrderContainer.java @@ -0,0 +1,50 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OrderColumn; +import java.util.ArrayList; +import java.util.List; + +/** + * This is a persistent entity abstraction encapsulating a collection of metadata resolver ids + * for the purpose of maintaining an order of all persistent metadata resolvers which becomes significant during + * generation of XML metadata for the resolvers. + * + * Maintaining this separate entity enables UI layer for example to explicitly manipulate ordering e.g. use REST + * API to reorder resolvers, etc. + * + * @author Dmitriy Kopylenko + */ +@Entity +@EqualsAndHashCode +@NoArgsConstructor +@Getter +@Setter +@ToString +public class MetadataResolversPositionOrderContainer { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @JsonIgnore + protected Long id; + + @ElementCollection + @CollectionTable(name="METADATA_RESOLVER_POSITION_ORDER", joinColumns=@JoinColumn(name="METADATA_RESOLVER_POSITION_ORDER_CONTAINER_ID")) + @Column(name="METADATA_RESOLVER_RESOURCE_ID") + @OrderColumn + private List resourceIds = new ArrayList<>(); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolversPositionOrderContainerRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolversPositionOrderContainerRepository.java new file mode 100644 index 000000000..6afe35ded --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolversPositionOrderContainerRepository.java @@ -0,0 +1,13 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; +import org.springframework.data.repository.CrudRepository; + +/** + * Spring Data Repository API for persistence operations on instances of {@link MetadataResolversPositionOrderContainer}. + * + * @author Dmitriy Kopylenko + */ +public interface MetadataResolversPositionOrderContainerRepository + extends CrudRepository { +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DefaultMetadataResolversPositionOrderContainerService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DefaultMetadataResolversPositionOrderContainerService.java new file mode 100644 index 000000000..76ac86cea --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/DefaultMetadataResolversPositionOrderContainerService.java @@ -0,0 +1,77 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +import static com.google.common.collect.FluentIterable.from; +import static java.util.stream.Collectors.toList; + + +/** + * Default implementation of {@link MetadataResolversPositionOrderContainer}. + * + * @author Dmitriy Kopylenko + */ +public class DefaultMetadataResolversPositionOrderContainerService implements MetadataResolversPositionOrderContainerService { + + private MetadataResolversPositionOrderContainerRepository positionOrderContainerRepository; + + private MetadataResolverRepository metadataResolverRepository; + + public DefaultMetadataResolversPositionOrderContainerService(MetadataResolversPositionOrderContainerRepository positionOrderRepository, + MetadataResolverRepository metadataResolverRepository) { + this.positionOrderContainerRepository = positionOrderRepository; + this.metadataResolverRepository = metadataResolverRepository; + } + + @Override + @Transactional + public void addOrUpdatePositionOrderContainer(MetadataResolversPositionOrderContainer metadataResolversPositionOrderContainer) { + MetadataResolversPositionOrderContainer existingPositionOrder = getPositionOrderContainerIfExists().orElse(null); + if (existingPositionOrder != null) { + existingPositionOrder.setResourceIds(metadataResolversPositionOrderContainer.getResourceIds()); + positionOrderContainerRepository.save(existingPositionOrder); + return; + } + positionOrderContainerRepository.save(metadataResolversPositionOrderContainer); + } + + @Override + @Transactional(readOnly = true) + public List getAllMetadataResolversInDefinedOrderOrUnordered() { + Optional orderContainer = getPositionOrderContainerIfExists(); + if(orderContainer.isPresent()) { + return orderContainer.get().getResourceIds() + .stream() + .map(metadataResolverRepository::findByResourceId) + .collect(toList()); + } + + return from(metadataResolverRepository.findAll()).toList(); + } + + @Override + public MetadataResolversPositionOrderContainer retrieveExistingOrEmpty() { + return getPositionOrderContainerIfExists().orElseGet(MetadataResolversPositionOrderContainer::new); + } + + @Override + @Transactional + public void appendPositionOrderForNew(MetadataResolver metadataResolver) { + MetadataResolversPositionOrderContainer positionOrderContainer = retrieveExistingOrEmpty(); + positionOrderContainer.getResourceIds().add(metadataResolver.getResourceId()); + positionOrderContainerRepository.save(positionOrderContainer); + } + + private Optional getPositionOrderContainerIfExists() { + Iterator iter = positionOrderContainerRepository.findAll().iterator(); + return iter.hasNext() ? Optional.of(iter.next()) : Optional.empty(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerService.java new file mode 100644 index 000000000..7de6490af --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerService.java @@ -0,0 +1,23 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer; + +import java.util.List; + +/** + * Service interface for manipulation of instances of {@link MetadataResolversPositionOrderContainer} and + * to abstract away MetadataResolvers ordering logic. + * + * @author Dmitriy Kopylenko + */ +public interface MetadataResolversPositionOrderContainerService { + + MetadataResolversPositionOrderContainer retrieveExistingOrEmpty(); + + void addOrUpdatePositionOrderContainer(MetadataResolversPositionOrderContainer metadataResolversPositionOrderContainer); + + List getAllMetadataResolversInDefinedOrderOrUnordered(); + + void appendPositionOrderForNew(MetadataResolver metadataResolver); +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerServiceTests.groovy new file mode 100644 index 000000000..de4af7e8e --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/MetadataResolversPositionOrderContainerServiceTests.groovy @@ -0,0 +1,53 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolversPositionOrderContainer +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository +import spock.lang.Specification +import spock.lang.Subject + +/** + * @author Dmitriy Kopylenko + */ +class MetadataResolversPositionOrderContainerServiceTests extends Specification { + + def "no order container has been provided and saved, so using unordered persisted resolvers"() { + given: + def resolverRepo = Mock(MetadataResolverRepository) + resolverRepo.findAll() >> [new MetadataResolver(resourceId: 'second'), new MetadataResolver(resourceId: 'first')] + def positionOrderRepo = Mock(MetadataResolversPositionOrderContainerRepository) + positionOrderRepo.findAll() >> [] + @Subject + def positionContainerSvc = new DefaultMetadataResolversPositionOrderContainerService(positionOrderRepo, resolverRepo) + + when: + def unorderedResolvers = positionContainerSvc.getAllMetadataResolversInDefinedOrderOrUnordered() + + then: + unorderedResolvers[0].resourceId == 'second' + unorderedResolvers[1].resourceId == 'first' + + } + + def "an order container has been provided and saved, so using resolvers with order defined in that position order container"() { + given: + def resolverRepo = Mock(MetadataResolverRepository) + resolverRepo.findAll() >> [new MetadataResolver(resourceId: 'second'), new MetadataResolver(resourceId: 'first')] + resolverRepo.findByResourceId('first') >> new MetadataResolver(resourceId: 'first') + resolverRepo.findByResourceId('second') >> new MetadataResolver(resourceId: 'second') + def positionOrderRepo = Mock(MetadataResolversPositionOrderContainerRepository) + positionOrderRepo.findAll() >> [new MetadataResolversPositionOrderContainer(resourceIds: ['first', 'second'])] + positionOrderRepo.findAll() >> [] + @Subject + def positionContainerSvc = new DefaultMetadataResolversPositionOrderContainerService(positionOrderRepo, resolverRepo) + + when: + def orderedResolvers = positionContainerSvc.getAllMetadataResolversInDefinedOrderOrUnordered() + + then: + orderedResolvers[0].resourceId == 'first' + orderedResolvers[1].resourceId == 'second' + + } +}