diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderController.java new file mode 100644 index 000000000..4c7d70ec0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderController.java @@ -0,0 +1,83 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.controller.support.RestControllersSupport; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toList; + +/** + * @author Dmitriy Kopylenko + */ +@RestController +@RequestMapping("/api/MetadataResolvers/{metadataResolverId}/FiltersPositionOrder") +public class MetadataFiltersPositionOrderController { + + @Autowired + MetadataResolverRepository metadataResolverRepository; + + @Autowired + RestControllersSupport restControllersSupport; + + @PostMapping + @Transactional + public ResponseEntity updateFiltersPositionOrder(@PathVariable String metadataResolverId, + @RequestBody List filtersResourceIds) { + + MetadataResolver resolver = restControllersSupport.findResolverOrThrowHttp404(metadataResolverId); + List currentFilters = resolver.getMetadataFilters(); + + //Check for bad data upfront. We could avoid this check and take wrong size and/or filter ids and blindly pass to sort below. + //In that case, the sort operation will silently NOT do anything and leave original filters order, + //but we will not be able to indicate to calling clients HTTP 400 in that case. + if ((filtersResourceIds.size() != currentFilters.size()) || + (!currentFilters.stream() + .map(MetadataFilter::getResourceId) + .collect(toList()) + .containsAll(filtersResourceIds))) { + + return ResponseEntity + .badRequest() + .body("Number of filters to reorder or filters resource ids do not match current filters"); + } + + //This is needed in order to set reference to persistent filters collection to be able to merge the persistent collection + //Otherwise if we manipulate the original collection directly and try to save, we'll get RDBMS constraint violation exception + List reOrderedFilters = new ArrayList<>(currentFilters); + + //Main re-ordering operation + reOrderedFilters.sort(Comparator.comparingInt(f -> filtersResourceIds.indexOf(f.getResourceId()))); + + //re-set the reference and save to DB + resolver.setMetadataFilters(reOrderedFilters); + metadataResolverRepository.save(resolver); + + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getFiltersPositionOrder(@PathVariable String metadataResolverId) { + MetadataResolver resolver = restControllersSupport.findResolverOrThrowHttp404(metadataResolverId); + List resourceIds = resolver.getMetadataFilters().stream() + .map(MetadataFilter::getResourceId) + .collect(toList()); + + return ResponseEntity.ok(resourceIds); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java new file mode 100644 index 000000000..1605b86dd --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/support/RestControllersSupport.java @@ -0,0 +1,41 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller.support; + +import com.google.common.collect.ImmutableMap; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpClientErrorException; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +/** + * Common functionality for REST controllers. + * + * @author Dmitriy Kopylenko + */ +@RestControllerAdvice +public class RestControllersSupport { + + @Autowired + MetadataResolverRepository resolverRepository; + + public MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) { + MetadataResolver resolver = resolverRepository.findByResourceId(resolverResourceId); + if(resolver == null) { + throw new HttpClientErrorException(NOT_FOUND, "Metadata resolver is not found"); + } + return resolver; + } + + + @ExceptionHandler + public ResponseEntity notFoundHandler(HttpClientErrorException ex) { + if(ex.getStatusCode() == NOT_FOUND) { + return ResponseEntity.status(NOT_FOUND).body(ex.getStatusText()); + } + throw ex; + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy new file mode 100644 index 000000000..4d163a660 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersPositionOrderControllerIntegrationTests.groovy @@ -0,0 +1,151 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.test.context.ActiveProfiles + +import spock.lang.Specification + + +/** + * @author Dmitriy Kopylenko + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("no-auth") +class MetadataFiltersPositionOrderControllerIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + @Autowired + MetadataResolverRepository metadataResolverRepository + + @Autowired + AttributeUtility attributeUtility + + TestObjectGenerator generator + + static BASE_URI = '/api/MetadataResolvers' + + static RESOURCE_URI = "$BASE_URI/%s/FiltersPositionOrder" + + def setup() { + generator = new TestObjectGenerator(attributeUtility) + } + + def cleanup() { + metadataResolverRepository.deleteAll() + } + + def "GET Filter Position Order for non-existent resolver"() { + when: 'GET request is made with resolver resource id NOT matching any existing filter' + def result = getFiltersPositionOrderFor('non-existent-resolver-id', String) + + then: "Request completed successfully" + result.statusCodeValue == 404 + } + + def "GET Default Filter Position Order for filters originally attached to a resolver"() { + given: 'MetadataResolver with 2 filters in data store' + def resolver = createResolverWithTwoFilters() + + when: 'GET request is made to retrieve position order of filters' + def result = getFiltersPositionOrderFor(resolver.resourceId, List) + + then: 'Original filters order is preserved' + result.statusCodeValue == 200 + result.body == [resolver.firstFilterResourceId, resolver.secondFilterResourceId] + } + + def "Reorder filters and verify the position within resolver persisted accordingly"() { + given: 'MetadataResolver with 2 filters in data store' + def resolver = createResolverWithTwoFilters() + def reOrderedFiltersPosition = [resolver.secondFilterResourceId, resolver.firstFilterResourceId] + + when: 'POST is made to re-order filters position' + def reorderPOSTResult = reorderFilters(resolver.resourceId, reOrderedFiltersPosition) + + then: 'Request completed successfully' + reorderPOSTResult.statusCodeValue == 204 + + and: 'GET request is made to retrieve position order of filters' + def positionOrderResult = getFiltersPositionOrderFor(resolver.resourceId, List) + + then: + positionOrderResult.body == reOrderedFiltersPosition + + and: "Request is made to retrieve the resolver with affected filters" + def resolverResult = getResolver(resolver.resourceId) + + then: + resolverResult.statusCodeValue == 200 + resolverResult.body.metadataFilters.collect {it.resourceId} == reOrderedFiltersPosition + } + + def "Reorder filters with bad data"() { + given: 'MetadataResolver with 2 filters in data store' + def resolver = createResolverWithTwoFilters() + def originalFiltersPosition = [resolver.firstFilterResourceId, resolver.secondFilterResourceId] + //Only one filter in order position data, while there are two filters + def reOrderedFiltersPosition = [resolver.secondFilterResourceId] + + when: 'POST is made to re-order filters position with invalid number of filters to re-order' + def reorderPOSTResult = reorderFilters(resolver.resourceId, reOrderedFiltersPosition) + + then: 'Request completed successfully with 400' + reorderPOSTResult.statusCodeValue == 400 + + and: 'GET request is made to retrieve position order of filters' + def positionOrderResult = getFiltersPositionOrderFor(resolver.resourceId, List) + + then: 'Original filters position order is retrieved' + positionOrderResult.body == originalFiltersPosition + + and: "Request is made to retrieve the resolver with original filters" + def resolverResult = getResolver(resolver.resourceId) + + then: + resolverResult.statusCodeValue == 200 + resolverResult.body.metadataFilters.collect {it.resourceId} == originalFiltersPosition + + and: 'POST is made to re-order filters position with invalid resource ids' + def reorderPOSTResult_2 = reorderFilters(resolver.resourceId, [resolver.secondFilterResourceId, 'invalid-id']) + + then: 'Request completed successfully with 400' + reorderPOSTResult_2.statusCodeValue == 400 + } + + private createResolverWithTwoFilters() { + def resolver = generator.buildRandomMetadataResolverOfType('FileBacked') + resolver.metadataFilters = [generator.signatureValidationFilter(), generator.entityRoleWhitelistFilter()] + def resolverResourceId = resolver.resourceId + def firstFilterResourceId = resolver.metadataFilters[0].resourceId + def secondFilterResourceId = resolver.metadataFilters[1].resourceId + metadataResolverRepository.save(resolver) + + [resourceId : resolverResourceId, + firstFilterResourceId : firstFilterResourceId, + secondFilterResourceId: secondFilterResourceId] + } + + private getFiltersPositionOrderFor(String resourceId, responseType) { + this.restTemplate.getForEntity(resourceUriFor(resourceId), responseType) + } + + private reorderFilters(String resourceId, List filterIdsPositionOrderList) { + this.restTemplate.postForEntity(resourceUriFor(resourceId), filterIdsPositionOrderList, null) + } + + private getResolver(String resolverResourceId) { + this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", Object) + } + + private static resourceUriFor(String resolverResourceId) { + String.format(RESOURCE_URI, resolverResourceId) + } +} \ No newline at end of file