From 34017c5b11c11acfa03613cebdc8805d451cb690 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 18 Mar 2021 15:20:44 -0700 Subject: [PATCH 1/3] SHIBUI-1750 Updated controller for entity query to support the MDQ spec. Added the spring etags filter. --- .../ui/configuration/ETagsConfiguration.java | 17 ++++ .../ui/controller/EntitiesController.java | 28 ++++--- .../controller/EntitiesControllerTests.groovy | 83 +++++++++++++++++++ 3 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/ETagsConfiguration.java 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..e96b7b5b0 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,16 +1,14 @@ 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.ResponseEntity; import org.springframework.stereotype.Controller; @@ -18,15 +16,19 @@ 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; 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 ea265d46d..e906cb058 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 @@ -64,6 +64,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,6 +80,15 @@ class EntitiesControllerTests extends Specification { result.andExpect(status().isNotFound()) } + 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 = ''' @@ -108,6 +125,42 @@ 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":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":{}, + "attributeRelease":["givenName","employeeNumber"] + } + ''' + when: + def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) + + then: + def x = content() + result.andExpect(status().isOk()) + .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 = ''' @@ -137,4 +190,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)) + } } From 90ede97d05a20b1d8524560720ef21ab71dfdebd Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 4 May 2021 15:57:24 -0700 Subject: [PATCH 2/3] SHIBUI-1750 Updated test with notes about headers --- .../shibboleth/admin/ui/controller/EntitiesController.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 e96b7b5b0..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 @@ -10,6 +10,7 @@ import org.opensaml.saml.metadata.resolver.MetadataResolver; import org.opensaml.saml.saml2.metadata.EntityDescriptor; 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; @@ -45,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") From 28730e924d4ce02dcb38b408b6815f40de3c73ac Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 4 May 2021 17:53:03 -0700 Subject: [PATCH 3/3] SHIBUI-1750 updated unit test --- .../controller/EntitiesControllerTests.groovy | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) 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 88d0427a8..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]) @@ -132,9 +133,8 @@ class EntitiesControllerTests extends Specification { "id":null, "serviceProviderName":null, "entityId":"http://test.scaldingspoon.org/test1", - "organization":null, + "organization": {}, "contacts":null, - "mdui":null, "serviceProviderSsoDescriptor": { "protocolSupportEnum":"SAML 2", "nameIdFormats":["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"] @@ -147,18 +147,26 @@ class EntitiesControllerTests extends Specification { "serviceEnabled":false, "createdDate":null, "modifiedDate":null, - "relyingPartyOverrides":{}, - "attributeRelease":["givenName","employeeNumber"] + "attributeRelease":["givenName","employeeNumber"], + "version":-1891841119, + "createdBy":null, + "current":false } ''' when: def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) then: - def x = content() + // 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(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedBody, false)) + .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'() {