diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java new file mode 100644 index 000000000..a409c8909 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +@Configuration +public class ETagsConfiguration { + @Bean + public FilterRegistrationBean shallowEtagHeaderFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + filterRegistrationBean.addUrlPatterns("/api/entities/*", "/entities/*"); + filterRegistrationBean.setName("etagFilter"); + return filterRegistrationBean; + } +} 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 index 8a5172fb6..81539913b 100644 --- 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 @@ -1,32 +1,35 @@ 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 java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import javax.servlet.http.HttpServletRequest; + 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.HttpHeaders; 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; +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 lombok.extern.slf4j.Slf4j; +import net.shibboleth.utilities.java.support.resolver.CriteriaSet; +import net.shibboleth.utilities.java.support.resolver.ResolverException; @Controller -@RequestMapping(value = "/api/entities", method = RequestMethod.GET) +@RequestMapping(value = { "/entities", // per protocol - https://spaces.at.internet2.edu/display/MDQ/Metadata+Query+Protocol + "/api/entities" }, // existing - included to break no existing code + method = RequestMethod.GET) +@Slf4j public class EntitiesController { - private static final Logger logger = LoggerFactory.getLogger(EntitiesController.class); - @Autowired private MetadataResolver metadataResolver; @@ -43,7 +46,8 @@ public ResponseEntity getOne(final @PathVariable String entityId, HttpServlet return ResponseEntity.notFound().build(); } EntityDescriptorRepresentation entityDescriptorRepresentation = entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor); - return ResponseEntity.ok(entityDescriptorRepresentation); + ResponseEntity result = ResponseEntity.ok(entityDescriptorRepresentation); + return result; } @RequestMapping(value = "{entityId:.*}", produces = "application/xml") 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 index 1eb4e7159..380c84e4f 100644 --- 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 @@ -15,6 +15,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.core.io.ClassPathResource import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.http.HttpHeaders import org.springframework.http.MediaType import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders @@ -23,7 +24,7 @@ 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 +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* @DataJpaTest @ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @@ -64,6 +65,14 @@ class EntitiesControllerTests extends Specification { result.andExpect(status().isNotFound()) } + def 'GET /entities/test'() { + when: + def result = mockMvc.perform(get("/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')) @@ -72,7 +81,14 @@ class EntitiesControllerTests extends Specification { result.andExpect(status().isNotFound()) } - //todo review + def 'GET /entities/test XML'() { + when: + def result = mockMvc.perform(get("/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 = ''' @@ -110,6 +126,49 @@ class EntitiesControllerTests extends Specification { .andExpect(content().json(expectedBody, false)) } + def 'GET /entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { + given: + def expectedBody = ''' + { + "id":null, + "serviceProviderName":null, + "entityId":"http://test.scaldingspoon.org/test1", + "organization": {}, + "contacts":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, + "attributeRelease":["givenName","employeeNumber"], + "version":-1891841119, + "createdBy":null, + "current":false + } + ''' + when: + def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) + + then: + // Response headers section 2.5 + // from the spec https://www.ietf.org/archive/id/draft-young-md-query-14.txt + result.andExpect(status().isOk()) + .andExpect(header().exists(HttpHeaders.CONTENT_TYPE)) // MUST HAVE +// .andExpect(header().exists(HttpHeaders.CONTENT_LENGTH)) // SHOULD HAVE - should end up from etag filter, so skipped for test +// .andExpect(header().exists(HttpHeaders.CACHE_CONTROL)) // SHOULD HAVE - should be included by Spring Security +// .andExpect(header().exists(HttpHeaders.LAST_MODIFIED)) // SHOULD HAVE - should end up from etag filter, so skipped for test +// .andExpect(header().exists(HttpHeaders.ETAG)) // MUST HAVE - is done by filter, so skipped for test + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json(expectedBody, false)) + } + def 'GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML'() { given: def expectedBody = ''' @@ -139,4 +198,34 @@ class EntitiesControllerTests extends Specification { .andExpect(content().contentType('application/xml;charset=ISO-8859-1')) .andExpect(content().xml(expectedBody)) } + + def 'GET /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('/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)) + } }