diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderController.java new file mode 100644 index 000000000..866f54231 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderController.java @@ -0,0 +1,111 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.repository.DynamicHttpMetadataResolverRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@RestController +@RequestMapping("/api/MetadataProvider/DynamicHttp") +public class DynamicHttpMetadataProviderController { + private static final Logger logger = LoggerFactory.getLogger(DynamicHttpMetadataProviderController.class); + + @Autowired + DynamicHttpMetadataResolverRepository repository; + + @DeleteMapping("/{resourceId}") + public ResponseEntity deleteByResourceId(@PathVariable String resourceId) { + if (repository.deleteByResourceId(resourceId)) { + return ResponseEntity.accepted().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @GetMapping("/name/{metadataProviderName}") + @Transactional(readOnly = true) + public ResponseEntity getOneByName(@PathVariable String metadataProviderName) { + DynamicHttpMetadataResolver resolver = repository.findByName(metadataProviderName); + if (resolver == null) { + return ResponseEntity.notFound().build(); + } else { + resolver.setVersion(resolver.hashCode()); + return ResponseEntity.ok(resolver); + } + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOneByResourceId(@PathVariable String resourceId) { + DynamicHttpMetadataResolver resolver = repository.findByResourceId(resourceId); + if (resolver == null) { + return ResponseEntity.notFound().build(); + } else { + resolver.setVersion(resolver.hashCode()); + return ResponseEntity.ok(resolver); + } + } + + @PostMapping + public ResponseEntity create(@RequestBody DynamicHttpMetadataResolver resolver) { + if (repository.findByName(resolver.getName()) != null) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + + DynamicHttpMetadataResolver persistedResolver = repository.save(resolver); + persistedResolver.setVersion(persistedResolver.hashCode()); + + return ResponseEntity + .created(getResourceUriFor(persistedResolver)) + .body(persistedResolver); + } + + @PutMapping + public ResponseEntity update(@RequestBody DynamicHttpMetadataResolver resolver) { + DynamicHttpMetadataResolver existingResolver = repository.findByResourceId(resolver.getResourceId()); + + if (existingResolver == null) { + return ResponseEntity.notFound().build(); + } + + if (existingResolver.hashCode() != resolver.getVersion()) { + logger.info("Comparing: " + existingResolver.hashCode() + " with " + resolver.getVersion()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + + resolver.setAudId(existingResolver.getAudId()); + //TODO: Do we need to set anything else? dates? + + DynamicHttpMetadataResolver updatedResolver = repository.save(resolver); + updatedResolver.setVersion(updatedResolver.hashCode()); + + return ResponseEntity.ok(updatedResolver); + } + + private static URI getResourceUriFor(DynamicHttpMetadataResolver resolver) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/MetadataProvider/DynamicHttp/") + .pathSegment(resolver.getResourceId()) + .build() + .toUri(); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java new file mode 100644 index 000000000..886839bf8 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java @@ -0,0 +1,39 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.ElementCollection; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.OrderColumn; +import java.util.List; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Entity +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Getter +@Setter +@ToString +public class DynamicHttpMetadataResolver extends MetadataResolver { + + @Embedded + private DynamicMetadataResolverAttributes dynamicMetadataResolverAttributes; + + @Embedded + private HttpMetadataResolverAttributes httpMetadataResolverAttributes; + + private int maxConnectionsTotal; + + private int maxConnectionsPerRoute; + + @ElementCollection + @OrderColumn + private List supportedContentTypes; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/HttpMetadataResolverAttributes.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/HttpMetadataResolverAttributes.java index 437516ffd..f8612a60f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/HttpMetadataResolverAttributes.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/HttpMetadataResolverAttributes.java @@ -52,7 +52,7 @@ public class HttpMetadataResolverAttributes { private Integer httpMaxCacheEntrySize; - private enum HttpCachingType { + public enum HttpCachingType { none,file,memory } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/DynamicHttpMetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/DynamicHttpMetadataResolverRepository.java new file mode 100644 index 000000000..c4a08804f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/DynamicHttpMetadataResolverRepository.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver; +import org.springframework.data.repository.CrudRepository; + +/** + * Spring Data CRUD repository for instances of {@link DynamicHttpMetadataResolver}s. + * + * @author Bill Smith (wsmith@unicon.net) + */ +public interface DynamicHttpMetadataResolverRepository extends CrudRepository { + + DynamicHttpMetadataResolver findByName(String name); + + boolean deleteByResourceId(String resourceId); + + DynamicHttpMetadataResolver findByResourceId(String resourceId); +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderControllerTests.groovy new file mode 100644 index 000000000..bf4ce2ffe --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderControllerTests.groovy @@ -0,0 +1,271 @@ +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.repository.DynamicHttpMetadataResolverRepository +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 groovy.json.JsonOutput +import groovy.json.JsonSlurper +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.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 DynamicHttpMetadataProviderControllerTests extends Specification { + RandomGenerator randomGenerator + TestObjectGenerator testObjectGenerator + ObjectMapper mapper + + def repository = Mock(DynamicHttpMetadataResolverRepository) + def controller + def mockMvc + + @Autowired + AttributeUtility attributeUtility + + def setup() { + randomGenerator = new RandomGenerator() + testObjectGenerator = new TestObjectGenerator(attributeUtility) + mapper = new ObjectMapper() + + controller = new DynamicHttpMetadataProviderController ( + repository: repository + ) + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } + + def "DELETE deletes the desired resolver"() { + given: + def randomResourceId = randomGenerator.randomId() + + 1 * repository.deleteByResourceId(randomResourceId) >> true + + when: + def result = mockMvc.perform( + delete("/api/MetadataProvider/DynamicHttp/$randomResourceId")) + + then: + result.andExpect(status().isAccepted()) + } + + def "DELETE returns error when desired resolver is not found"() { + given: + def randomResourceId = randomGenerator.randomId() + + 1 * repository.deleteByResourceId(randomResourceId) >> false + + when: + def result = mockMvc.perform( + delete("/api/MetadataProvider/DynamicHttp/$randomResourceId")) + + then: + result.andExpect(status().isNotFound()) + } + + def "POST a new resolver properly persists and returns the new persisted resolver"() { + given: + def resolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + resolver.setVersion(resolver.hashCode()) + def postedJsonBody = mapper.writeValueAsString(resolver) + + 1 * repository.findByName(resolver.getName()) >> null + 1 * repository.save(_) >> resolver + + def expectedResolverUUID = resolver.getResourceId() + def expectedResponseHeader = 'Location' + def expectedResponseHeaderValue = "/api/MetadataProvider/DynamicHttp/$expectedResolverUUID" + + when: + def result = mockMvc.perform( + post('/api/MetadataProvider/DynamicHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().json(postedJsonBody, false)) + .andExpect(header().string(expectedResponseHeader, containsString(expectedResponseHeaderValue))) + + } + + def "POST a new resolver that has a name of a persisted resolver returns conflict"() { + given: + def resolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + def resolverName = resolver.name + def postedJsonBody = mapper.writeValueAsString(resolver) + + 1 * repository.findByName(resolverName) >> resolver + 0 * repository.save(_) + + when: + def result = mockMvc.perform( + post('/api/MetadataProvider/DynamicHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isConflict()) + } + + def "GET by resourceId returns the desired persisted resolver"() { + given: + def resolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + resolver.version = resolver.hashCode() + def resourceId = resolver.resourceId + def resolverJson = mapper.writeValueAsString(resolver) + + 1 * repository.findByResourceId(resourceId) >> resolver + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/DynamicHttp/$resourceId")) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(resolverJson, false)) + } + + def "GET by unknown resource id returns not found"() { + given: + def randomResourceId = randomGenerator.randomId() + + 1 * repository.findByResourceId(randomResourceId) >> null + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/DynamicHttp/$randomResourceId")) + + then: + result.andExpect(status().isNotFound()) + } + + def "GET by resolver name returns the desired persisted resolver"() { + given: + def resolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + resolver.version = resolver.hashCode() + def resolverName = resolver.name + def resolverJson = mapper.writeValueAsString(resolver) + + 1 * repository.findByName(resolverName) >> resolver + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/DynamicHttp/name/$resolverName")) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(resolverJson, false)) + } + + def "GET by unknown resolver name returns not found"() { + given: + def randomResolverName = randomGenerator.randomString(10) + + 1 * repository.findByName(randomResolverName) >> null + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/DynamicHttp/name/$randomResolverName")) + + then: + result.andExpect(status().isNotFound()) + } + + def "PUT allows for a successful update of an already-persisted resolver"() { + given: + def existingResolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + existingResolver.version = existingResolver.hashCode() + def resourceId = existingResolver.resourceId + def existingResolverJson = mapper.writeValueAsString(existingResolver) + + def updatedResolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + updatedResolver.resourceId = existingResolver.resourceId + updatedResolver.version = existingResolver.version + def postedJsonBody = mapper.writeValueAsString(updatedResolver) + + 1 * repository.findByResourceId(existingResolver.resourceId) >> existingResolver + 1 * repository.save(_) >> updatedResolver + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform( + put('/api/MetadataProvider/DynamicHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + def expectedJson = new JsonSlurper().parseText(postedJsonBody) + expectedJson << [version: updatedResolver.hashCode()] + result.andExpect(status().isOk()) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(JsonOutput.toJson(expectedJson), false)) + } + + def "PUT of an updated resolver with an incorrect version returns a conflict"() { + given: + def existingResolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + existingResolver.version = existingResolver.hashCode() + + def updatedResolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + updatedResolver.resourceId = existingResolver.resourceId + updatedResolver.version = updatedResolver.hashCode() + def postedJsonBody = mapper.writeValueAsString(updatedResolver) + + 1 * repository.findByResourceId(existingResolver.resourceId) >> existingResolver + 0 * repository.save(_) + + when: + def result = mockMvc.perform( + put('/api/MetadataProvider/DynamicHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isConflict()) + } + + def "PUT of a resolver that is not persisted returns not found"() { + given: + def resolver = testObjectGenerator.buildDynamicHttpMetadataResolver() + def postedJsonBody = mapper.writeValueAsString(resolver) + + 1 * repository.findByResourceId(resolver.resourceId) >> null + 0 * repository.save(_) + + when: + def result = mockMvc.perform( + put('/api/MetadataProvider/DynamicHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isNotFound()) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index cd5026ede..cea76c233 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -14,7 +14,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter 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.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.MDDCConstants @@ -35,6 +37,52 @@ class TestObjectGenerator { this.attributeUtility = attributeUtility } + DynamicHttpMetadataResolver buildDynamicHttpMetadataResolver() { + def resolver = new DynamicHttpMetadataResolver().with { + it.dynamicMetadataResolverAttributes = buildDynamicMetadataResolverAttributes() + it.httpMetadataResolverAttributes = buildHttpMetadataResolverAttributes() + it.maxConnectionsPerRoute = generator.randomInt(1, 100) + it.maxConnectionsTotal = generator.randomInt(1, 100) + it.supportedContentTypes = generator.randomStringList() + it.name = generator.randomString(10) + it.requireValidMetadata = generator.randomBoolean() + it.failFastInitialization = generator.randomBoolean() + it.sortKey = generator.randomInt(1, 10) + it.criterionPredicateRegistryRef = generator.randomString(10) + it.useDefaultPredicateRegistry = generator.randomBoolean() + it.satisfyAnyPredicates = generator.randomBoolean() + it.metadataFilters = buildAllTypesOfFilterList() + it + } + return resolver + } + + HttpMetadataResolverAttributes buildHttpMetadataResolverAttributes() { + def attributes = new HttpMetadataResolverAttributes().with { + it.disregardTLSCertificate = generator.randomBoolean() + it.connectionRequestTimeout = generator.randomString(10) + it.httpClientRef = generator.randomString(10) + it.httpCacheDirectory = generator.randomString(10) + it.httpCaching = randomHttpCachingType() + it.httpClientSecurityParametersRef = generator.randomString(10) + it.httpMaxCacheEntries = generator.randomInt(1, 10) + it.httpMaxCacheEntrySize = generator.randomInt(100, 10000) + it.proxyHost = generator.randomString(10) + it.proxyPassword = generator.randomString(10) + it.proxyPort = generator.randomString(5) + it.proxyUser = generator.randomString(10) + it.requestTimeout = generator.randomString(10) + it.socketTimeout = generator.randomString(10) + it.tlsTrustEngineRef = generator.randomString(10) + it + } + return attributes + } + + HttpMetadataResolverAttributes.HttpCachingType randomHttpCachingType() { + HttpMetadataResolverAttributes.HttpCachingType.values()[generator.randomInt(0, 2)] + } + LocalDynamicMetadataResolver buildLocalDynamicMetadataResolver() { def resolver = new LocalDynamicMetadataResolver().with { it.dynamicMetadataResolverAttributes = buildDynamicMetadataResolverAttributes()