From ba6ffc2dd98dda834022e98705330f5e240f2006 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 18 Mar 2021 11:48:32 -0700 Subject: [PATCH 01/16] NOJIRA Adding Eclipse bits to ignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 76178c9b0..cb8ad4931 100644 --- a/.gitignore +++ b/.gitignore @@ -400,3 +400,10 @@ rdurable build-no-tests beacon/spring/out +/.classpath + +# Eclipse junk +*.classpath +*.settings +*.project +*bin \ No newline at end of file From 34017c5b11c11acfa03613cebdc8805d451cb690 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 18 Mar 2021 15:20:44 -0700 Subject: [PATCH 02/16] 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 171b3243dbf82caf59fb7419867d1118399005d7 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 31 Mar 2021 14:50:28 -0700 Subject: [PATCH 03/16] SHIBUI-1786 SHIBUI-1734 Created entity for custom entity attributes and JPA repo for operations --- .../admin/ui/domain/CustomAttribute.java | 44 +++++ .../repository/CustomAttributeRepository.java | 26 +++ .../CustomAttributeRepositoryTests.groovy | 156 ++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java new file mode 100644 index 000000000..9c1574be6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import java.util.HashSet; +import java.util.Set; + +import javax.persistence.CollectionTable; +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.JoinColumn; + +import org.hibernate.envers.Audited; + +import lombok.Data; + +@Entity(name = "custom_attribute") +@Audited +@Data +public class CustomAttribute { + @Id + @Column(nullable = false) + String name; + + @Column(name = "help_text", nullable = true) + String helpText; + + @Column(name = "attribute_type", nullable = false) + CustomAttributeType attributeType; + + @Column(name = "default_value", nullable = true) + String defaultValue; + + @ElementCollection + @CollectionTable(name = "custom_attr_values", joinColumns = @JoinColumn(name = "name")) + @Column(name = "value", nullable = false) + Set customAttrValues = new HashSet<>(); + + // @TODO: logic to ensure defaultValue matches an item from the list of values when SELECTION_LIST is the type ?? +} + +enum CustomAttributeType { + BOOLEAN, INTEGER, LONG, DOUBLE, DURATION, SELECTION_LIST, SPRING_BEAN_ID +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java new file mode 100644 index 000000000..edbc4cd9b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java @@ -0,0 +1,26 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import java.util.Date; +import java.util.List; + +import javax.transaction.Transactional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttribute; + +/** + * Repository to manage {@link CustomAttribute} instances. + */ +public interface CustomAttributeRepository extends JpaRepository { + + List findAll(); + + CustomAttribute findByName(String name); + + @SuppressWarnings("unchecked") + CustomAttribute save(CustomAttribute attribute); +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy new file mode 100644 index 000000000..9aa14cab0 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy @@ -0,0 +1,156 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository + +import javax.persistence.EntityManager + +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 edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttribute +import spock.lang.Specification + +/** + * Tests to validate the repo and model for custom entity attributes + * @author chasegawa + */ +@DataJpaTest +@ContextConfiguration(classes=[InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class CustomAttributeRepositoryTests extends Specification { + + @Autowired + CustomAttributeRepository repo + + @Autowired + EntityManager entityManager + + def "basic CRUD operations validated"() { + given: + def setItems = new HashSet(["val1", "val2", "val3"]) + def ca = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems + it + } + + // Confirm empty state + when: + def atts = repo.findAll() + + then: + atts.size() == 0 + + // save check + when: + repo.save(ca) + entityManager.flush() + entityManager.clear() + + then: + // save check + def cas = repo.findAll() + cas.size() == 1 + def caFromDb1 = cas.get(0).asType(CustomAttribute) + caFromDb1.equals(ca) == true + + // fetch checks + repo.findByName("not a name") == null + repo.findByName("ca-name").equals(ca) + + // update check + caFromDb1.with { + it.helpText = "some new text that wasn't there before" + } + caFromDb1.equals(ca) == false + + when: + repo.save(caFromDb1) + entityManager.flush() + entityManager.clear() + + then: + def cas2 = repo.findAll() + cas2.size() == 1 + def caFromDb2 = cas2.get(0).asType(CustomAttribute) + caFromDb2.equals(ca) == false + caFromDb2.equals(caFromDb1) == true + + // delete tests + when: + def delByName = new CustomAttribute().with { + it.name = "ca-name" + it + } + repo.delete(delByName) + entityManager.flush() + entityManager.clear() + + then: + repo.findAll().size() == 0 + } + + def "attribute list tests"() { + given: + def setItems2 = new HashSet(["val2", "val1"]) + def setItems3 = new HashSet(["val1", "val2", "val3"]) + def setItems4 = new HashSet(["val1", "val2", "val3", "val4"]) + def ca2 = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems2 + it + } + def ca3 = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems3 + it + } + def ca4 = new CustomAttribute().with { + it.name = "ca-name" + it.attributeType = "SELECTION_LIST" + it.customAttrValues = setItems4 + it + } + + when: + repo.save(ca3) + entityManager.flush() + entityManager.clear() + + then: + def cas = repo.findAll() + cas.size() == 1 + def caFromDb = cas.get(0).asType(CustomAttribute) + caFromDb.equals(ca3) == true + + // now update the attribute list items + caFromDb.with { + it.customAttrValues = setItems4 + it + } + repo.save(caFromDb) + entityManager.flush() + entityManager.clear() + + def caFromDb4 = repo.findAll().get(0).asType(CustomAttribute) + caFromDb4.equals(ca4) == true + + // now remove items + caFromDb.with { + it.customAttrValues = setItems2 + it + } + repo.save(caFromDb) + entityManager.flush() + entityManager.clear() + + def caFromDb2 = repo.findAll().get(0).asType(CustomAttribute) + caFromDb2.equals(ca2) == true + } +} \ No newline at end of file From ed2ec50d180280acc5dcc1f34f1d2359ae04230f Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 16 Apr 2021 08:06:59 -0700 Subject: [PATCH 04/16] SHIBUI-1734 Commit to branch - incomplete task --- ...te.java => CustomAttributeDefinition.java} | 8 ++--- .../repository/CustomAttributeRepository.java | 12 +++---- .../CustomAttributeRepositoryTests.groovy | 34 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) rename backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/{CustomAttribute.java => CustomAttributeDefinition.java} (80%) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttributeDefinition.java similarity index 80% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttributeDefinition.java index 9c1574be6..03b9db95d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttribute.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/CustomAttributeDefinition.java @@ -14,10 +14,10 @@ import lombok.Data; -@Entity(name = "custom_attribute") +@Entity(name = "custom_attribute_definition") @Audited @Data -public class CustomAttribute { +public class CustomAttributeDefinition { @Id @Column(nullable = false) String name; @@ -32,9 +32,9 @@ public class CustomAttribute { String defaultValue; @ElementCollection - @CollectionTable(name = "custom_attr_values", joinColumns = @JoinColumn(name = "name")) + @CollectionTable(name = "custom_attr_list_defs", joinColumns = @JoinColumn(name = "name")) @Column(name = "value", nullable = false) - Set customAttrValues = new HashSet<>(); + Set customAttrListDefinitions = new HashSet<>(); // @TODO: logic to ensure defaultValue matches an item from the list of values when SELECTION_LIST is the type ?? } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java index edbc4cd9b..ba6385154 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java @@ -10,17 +10,17 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttribute; +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttributeDefinition; /** - * Repository to manage {@link CustomAttribute} instances. + * Repository to manage {@link CustomAttributeDefinition} instances. */ -public interface CustomAttributeRepository extends JpaRepository { +public interface CustomAttributeRepository extends JpaRepository { - List findAll(); + List findAll(); - CustomAttribute findByName(String name); + CustomAttributeDefinition findByName(String name); @SuppressWarnings("unchecked") - CustomAttribute save(CustomAttribute attribute); + CustomAttributeDefinition save(CustomAttributeDefinition attribute); } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy index 9aa14cab0..f9af4b2a4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepositoryTests.groovy @@ -9,7 +9,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.context.ContextConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttribute +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttributeDefinition import spock.lang.Specification /** @@ -31,10 +31,10 @@ class CustomAttributeRepositoryTests extends Specification { def "basic CRUD operations validated"() { given: def setItems = new HashSet(["val1", "val2", "val3"]) - def ca = new CustomAttribute().with { + def ca = new CustomAttributeDefinition().with { it.name = "ca-name" it.attributeType = "SELECTION_LIST" - it.customAttrValues = setItems + it.customAttrListDefinitions = setItems it } @@ -55,7 +55,7 @@ class CustomAttributeRepositoryTests extends Specification { // save check def cas = repo.findAll() cas.size() == 1 - def caFromDb1 = cas.get(0).asType(CustomAttribute) + def caFromDb1 = cas.get(0).asType(CustomAttributeDefinition) caFromDb1.equals(ca) == true // fetch checks @@ -76,13 +76,13 @@ class CustomAttributeRepositoryTests extends Specification { then: def cas2 = repo.findAll() cas2.size() == 1 - def caFromDb2 = cas2.get(0).asType(CustomAttribute) + def caFromDb2 = cas2.get(0).asType(CustomAttributeDefinition) caFromDb2.equals(ca) == false caFromDb2.equals(caFromDb1) == true // delete tests when: - def delByName = new CustomAttribute().with { + def delByName = new CustomAttributeDefinition().with { it.name = "ca-name" it } @@ -99,22 +99,22 @@ class CustomAttributeRepositoryTests extends Specification { def setItems2 = new HashSet(["val2", "val1"]) def setItems3 = new HashSet(["val1", "val2", "val3"]) def setItems4 = new HashSet(["val1", "val2", "val3", "val4"]) - def ca2 = new CustomAttribute().with { + def ca2 = new CustomAttributeDefinition().with { it.name = "ca-name" it.attributeType = "SELECTION_LIST" - it.customAttrValues = setItems2 + it.customAttrListDefinitions = setItems2 it } - def ca3 = new CustomAttribute().with { + def ca3 = new CustomAttributeDefinition().with { it.name = "ca-name" it.attributeType = "SELECTION_LIST" - it.customAttrValues = setItems3 + it.customAttrListDefinitions = setItems3 it } - def ca4 = new CustomAttribute().with { + def ca4 = new CustomAttributeDefinition().with { it.name = "ca-name" it.attributeType = "SELECTION_LIST" - it.customAttrValues = setItems4 + it.customAttrListDefinitions = setItems4 it } @@ -126,31 +126,31 @@ class CustomAttributeRepositoryTests extends Specification { then: def cas = repo.findAll() cas.size() == 1 - def caFromDb = cas.get(0).asType(CustomAttribute) + def caFromDb = cas.get(0).asType(CustomAttributeDefinition) caFromDb.equals(ca3) == true // now update the attribute list items caFromDb.with { - it.customAttrValues = setItems4 + it.customAttrListDefinitions = setItems4 it } repo.save(caFromDb) entityManager.flush() entityManager.clear() - def caFromDb4 = repo.findAll().get(0).asType(CustomAttribute) + def caFromDb4 = repo.findAll().get(0).asType(CustomAttributeDefinition) caFromDb4.equals(ca4) == true // now remove items caFromDb.with { - it.customAttrValues = setItems2 + it.customAttrListDefinitions = setItems2 it } repo.save(caFromDb) entityManager.flush() entityManager.clear() - def caFromDb2 = repo.findAll().get(0).asType(CustomAttribute) + def caFromDb2 = repo.findAll().get(0).asType(CustomAttributeDefinition) caFromDb2.equals(ca2) == true } } \ No newline at end of file From 90ede97d05a20b1d8524560720ef21ab71dfdebd Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 4 May 2021 15:57:24 -0700 Subject: [PATCH 05/16] 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 06/16] 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'() { From d6625a1a6fb68e5913e0d62c63ac5c09cfb290c9 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 5 May 2021 17:25:26 -0700 Subject: [PATCH 07/16] SHIBUI-1734 Added service and controller to expose API for CRUD operations around custom attribute definitions --- .../CustomAttributesController.java | 104 ++++++++++++++++++ .../repository/CustomAttributeRepository.java | 6 - .../ui/service/CustomAttributesService.java | 17 +++ .../service/CustomAttributesServiceImpl.java | 36 ++++++ 4 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomAttributesController.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesService.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesServiceImpl.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomAttributesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomAttributesController.java new file mode 100644 index 000000000..31d6ca809 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/CustomAttributesController.java @@ -0,0 +1,104 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +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.servlet.support.ServletUriComponentsBuilder; + +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttributeDefinition; +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; +import edu.internet2.tier.shibboleth.admin.ui.service.CustomAttributesService; + +@Controller +@RequestMapping(value = "/api/custom") +public class CustomAttributesController { + @Autowired + private CustomAttributesService caService; + + @PostMapping("/attribute") + @Transactional + public ResponseEntity create(@RequestBody CustomAttributeDefinition definition) { + // If already defined, we can't create a new one, nor will this call update the definition + CustomAttributeDefinition cad = caService.find(definition.getName()); + + if (cad != null) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/attribute").build().toUri()); + + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), + String.format("The custom attribute definition with name: [%s] already exists.", definition.getName()))); + } + + CustomAttributeDefinition result = caService.createOrUpdateDefinition(definition); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @PutMapping("/attribute") + @Transactional + public ResponseEntity update(@RequestBody CustomAttributeDefinition definition) { + CustomAttributeDefinition cad = caService.find(definition.getName()); + if (cad == null) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/attribute").build().toUri()); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.NOT_FOUND.value()), + String.format("The custom attribute definition with name: [%s] does not already exist.", definition.getName()))); + } + + CustomAttributeDefinition result = caService.createOrUpdateDefinition(definition); + return ResponseEntity.ok(result); + } + + @GetMapping("/attributes") + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(caService.getAllDefinitions()); + } + + @GetMapping("/attribute/{name}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String name) { + CustomAttributeDefinition cad = caService.find(name); + if (cad == null) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation( + ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/attribute/" + name).build().toUri()); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.NOT_FOUND.value()), + String.format("The custom attribute definition with name: [%s] does not already exist.", name))); + } + return ResponseEntity.ok(cad); + } + + @DeleteMapping("/attribute/{name}") + @Transactional + public ResponseEntity delete(@PathVariable String name) { + CustomAttributeDefinition cad = caService.find(name); + if (cad == null) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation( + ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/custom/attribute/" + name).build().toUri()); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.NOT_FOUND.value()), + String.format("The custom attribute definition with name: [%s] does not already exist.", name))); + } + caService.deleteDefinition(cad); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java index ba6385154..3470e9834 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/CustomAttributeRepository.java @@ -1,14 +1,8 @@ package edu.internet2.tier.shibboleth.admin.ui.repository; -import java.util.Date; import java.util.List; -import javax.transaction.Transactional; - import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttributeDefinition; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesService.java new file mode 100644 index 000000000..6e2436bdf --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesService.java @@ -0,0 +1,17 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import java.util.List; + +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttributeDefinition; + +public interface CustomAttributesService { + + CustomAttributeDefinition createOrUpdateDefinition(CustomAttributeDefinition definition); + + void deleteDefinition(CustomAttributeDefinition definition); + + CustomAttributeDefinition find(String name); + + List getAllDefinitions(); + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesServiceImpl.java new file mode 100644 index 000000000..1dba4dae5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/CustomAttributesServiceImpl.java @@ -0,0 +1,36 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.CustomAttributeDefinition; +import edu.internet2.tier.shibboleth.admin.ui.repository.CustomAttributeRepository; + +@Service +public class CustomAttributesServiceImpl implements CustomAttributesService { + @Autowired + private CustomAttributeRepository repository; + + @Override + public CustomAttributeDefinition createOrUpdateDefinition(CustomAttributeDefinition definition) { + return repository.save(definition); + } + + @Override + public void deleteDefinition(CustomAttributeDefinition definition) { + repository.delete(definition); + } + + @Override + public CustomAttributeDefinition find(String name) { + return repository.findByName(name); + } + + @Override + public List getAllDefinitions() { + return repository.findAll(); + } + +} From 4b526555f9a4790f2758e4b994b206ecd957e882 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 7 May 2021 15:45:24 -0700 Subject: [PATCH 08/16] SHIBUI-1776 Corrects the logic being used to fetch the EntityDescriptor --- .../admin/ui/controller/EntitiesController.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 81539913b..302b807e8 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 @@ -13,12 +13,14 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; 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.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import lombok.extern.slf4j.Slf4j; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; @@ -30,16 +32,17 @@ method = RequestMethod.GET) @Slf4j public class EntitiesController { - @Autowired - private MetadataResolver metadataResolver; - @Autowired private EntityDescriptorService entityDescriptorService; @Autowired private OpenSamlObjects openSamlObjects; + + @Autowired + private EntityDescriptorRepository entityDescriptorRepository; - @RequestMapping(value = "{entityId:.*}") + @RequestMapping(value = "/{entityId:.*}") + @Transactional(readOnly = true) public ResponseEntity getOne(final @PathVariable String entityId, HttpServletRequest request) throws UnsupportedEncodingException, ResolverException { EntityDescriptor entityDescriptor = this.getEntityDescriptor(entityId); if (entityDescriptor == null) { @@ -50,7 +53,8 @@ public ResponseEntity getOne(final @PathVariable String entityId, HttpServlet return result; } - @RequestMapping(value = "{entityId:.*}", produces = "application/xml") + @RequestMapping(value = "/{entityId:.*}", produces = "application/xml") + @Transactional(readOnly = true) public ResponseEntity getOneXml(final @PathVariable String entityId) throws MarshallingException, ResolverException, UnsupportedEncodingException { EntityDescriptor entityDescriptor = this.getEntityDescriptor(entityId); if (entityDescriptor == null) { @@ -62,7 +66,7 @@ public ResponseEntity getOneXml(final @PathVariable String entityId) throws M private EntityDescriptor getEntityDescriptor(final String entityId) throws ResolverException, UnsupportedEncodingException { String decodedEntityId = URLDecoder.decode(entityId, "UTF-8"); - EntityDescriptor entityDescriptor = this.metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion(decodedEntityId))); + EntityDescriptor entityDescriptor = entityDescriptorRepository.findByEntityID(decodedEntityId); // TODO: we need to clean this up sometime if (entityDescriptor instanceof edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor) { ((edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor) entityDescriptor).setResourceId(null); From 3e476da55a4c320a59cc63a54b7acfcaf73b9944 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 7 May 2021 15:50:56 -0700 Subject: [PATCH 09/16] SHIBUI-1776 Code cleanup --- .../shibboleth/admin/ui/controller/EntitiesController.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 302b807e8..75b5be033 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 @@ -5,12 +5,9 @@ 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.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; @@ -23,7 +20,6 @@ import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; 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 @@ -49,8 +45,7 @@ public ResponseEntity getOne(final @PathVariable String entityId, HttpServlet return ResponseEntity.notFound().build(); } EntityDescriptorRepresentation entityDescriptorRepresentation = entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor); - ResponseEntity result = ResponseEntity.ok(entityDescriptorRepresentation); - return result; + return ResponseEntity.ok(entityDescriptorRepresentation); } @RequestMapping(value = "/{entityId:.*}", produces = "application/xml") From 05d6582b94afc5beb78a6384f7d512a2582ebf0b Mon Sep 17 00:00:00 2001 From: chasegawa Date: Fri, 7 May 2021 16:37:33 -0700 Subject: [PATCH 10/16] SHIBUI-1863 Added allow doubleslash in encoded urls --- .../admin/ui/configuration/auto/WebSecurityConfig.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java index cc6847621..f75f323be 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/WebSecurityConfig.java @@ -49,15 +49,14 @@ public class WebSecurityConfig { @Autowired private RoleRepository roleRepository; - @Bean - public HttpFirewall allowUrlEncodedSlashHttpFirewall() { + private HttpFirewall allowUrlEncodedSlashHttpFirewall() { StrictHttpFirewall firewall = new StrictHttpFirewall(); firewall.setAllowUrlEncodedSlash(true); + firewall.setAllowUrlEncodedDoubleSlash(true); return firewall; } - @Bean - public HttpFirewall defaultFirewall() { + private HttpFirewall defaultFirewall() { return new DefaultHttpFirewall(); } From c112d1e52e4256a04cc6372e9f0759950a14c97f Mon Sep 17 00:00:00 2001 From: Charles Hasegawa Date: Fri, 7 May 2021 23:59:23 +0000 Subject: [PATCH 11/16] Merged master into SHIBUI-1863 From deb5fc8caf0566bd6b7ccf42eaf950e7d1649e0a Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 18 May 2021 09:58:56 -0700 Subject: [PATCH 12/16] SHIBUI-1880 Added javadoc for clarity on the class' role --- .../shibboleth/admin/ui/controller/EntitiesController.java | 4 ++++ 1 file changed, 4 insertions(+) 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 75b5be033..927de19b4 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 @@ -27,6 +27,10 @@ "/api/entities" }, // existing - included to break no existing code method = RequestMethod.GET) @Slf4j +/** +* EntitiesController is here to meet the requirements for this project being an MDQ. Despite similar logic to the +* EntitiesDescriptorController, the required endpoints that make this project an MDQ server are served by this controller. +*/ public class EntitiesController { @Autowired private EntityDescriptorService entityDescriptorService; From 77d578911544f1b5d9889e7a74e66f63b8a75122 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 18 May 2021 12:25:04 -0700 Subject: [PATCH 13/16] SHIBUI-1880 Fixed EntitiesControllerTests --- .../controller/EntitiesControllerTests.groovy | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 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 380c84e4f..d7f41536b 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 @@ -5,11 +5,16 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration 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.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import net.shibboleth.ext.spring.resource.ResourceHelper +import net.shibboleth.utilities.java.support.resolver.CriteriaSet + +import org.opensaml.core.criterion.EntityIdCriterion import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver +import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -22,6 +27,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders import spock.lang.Specification import spock.lang.Subject +import static org.hamcrest.Matchers.is; 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.* @@ -31,11 +37,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntitiesControllerTests extends Specification { + + // Controller needs this to spit out the data def openSamlObjects = new OpenSamlObjects().with { init() it } + // resource will load the ED from the aggregate.xml file for testing def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) def metadataResolver = new ResourceBackedMetadataResolver(resource).with { @@ -47,12 +56,19 @@ class EntitiesControllerTests extends Specification { @Autowired UserService userService - + + // This stub will spit out the results from the resolver instead of actually finding them in the DB + @SpringBean + EntityDescriptorRepository edr = Stub(EntityDescriptorRepository) { + findByEntityID("http://test.scaldingspoon.org/test1") >> metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion("http://test.scaldingspoon.org/test1"))) + findByEntityID("test") >> metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion("test"))) + } + @Subject def controller = new EntitiesController( openSamlObjects: openSamlObjects, entityDescriptorService: new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects), userService), - metadataResolver: metadataResolver + entityDescriptorRepository: edr ) def mockMvc = MockMvcBuilders.standaloneSetup(controller).build() @@ -116,14 +132,21 @@ class EntitiesControllerTests extends Specification { "current":false } ''' + when: - def result = mockMvc.perform(get('/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) + 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 /entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1'() { @@ -153,6 +176,7 @@ class EntitiesControllerTests extends Specification { "current":false } ''' + when: def result = mockMvc.perform(get('/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1')) From 75c3657936b20dd6ae40c8dc48f6b9e62ec99439 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 18 May 2021 12:52:48 -0700 Subject: [PATCH 14/16] SHIBUI-1880 Fixes for all unit testing that was broken --- .../EntitiesControllerIntegrationTests.groovy | 31 ++++++++++++++++--- .../controller/EntitiesControllerTests.groovy | 3 -- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy index 01a4ff060..bda1b4dfa 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy @@ -1,14 +1,20 @@ package edu.internet2.tier.shibboleth.admin.ui.controller 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.security.service.UserService import groovy.json.JsonOutput import net.shibboleth.ext.spring.resource.ResourceHelper +import net.shibboleth.utilities.java.support.resolver.CriteriaSet + import org.joda.time.DateTime +import org.opensaml.core.criterion.EntityIdCriterion import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver +import org.spockframework.spring.SpringBean import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration @@ -35,11 +41,26 @@ class EntitiesControllerIntegrationTests extends Specification { @Autowired private WebTestClient webClient - /*def setup() { - // yeah, don't ask... this is just shenanigans - // The API is changed. Doesn't work anymore. Not sure if we need it here - this.webClient.webClient.uriBuilderFactory.encodingMode = DefaultUriBuilderFactory.EncodingMode.NONE - }*/ + def openSamlObjects = new OpenSamlObjects().with { + init() + it + } + + def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) + + def metadataResolver = new ResourceBackedMetadataResolver(resource).with { + it.id = 'test' + it.parserPool = openSamlObjects.parserPool + initialize() + it + } + + // This stub will spit out the results from the resolver instead of actually finding them in the DB + @SpringBean + EntityDescriptorRepository edr = Stub(EntityDescriptorRepository) { + findByEntityID("http://test.scaldingspoon.org/test1") >> metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion("http://test.scaldingspoon.org/test1"))) + findByEntityID("test") >> metadataResolver.resolveSingle(new CriteriaSet(new EntityIdCriterion("test"))) + } //todo review def "GET /api/entities returns the proper json"() { 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 d7f41536b..48e3bc8ae 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 @@ -37,14 +37,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntitiesControllerTests extends Specification { - - // Controller needs this to spit out the data def openSamlObjects = new OpenSamlObjects().with { init() it } - // resource will load the ED from the aggregate.xml file for testing def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) def metadataResolver = new ResourceBackedMetadataResolver(resource).with { From e08f9d7ca421e41f537bf7866503677b4f35fce0 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 18 May 2021 14:27:19 -0700 Subject: [PATCH 15/16] SHIBUI-1875 Modified response to include last modified header --- .../ui/controller/EntitiesController.java | 23 +++++++++++++++++-- .../EntityDescriptorRepresentation.java | 5 ++++ .../controller/EntitiesControllerTests.groovy | 6 ++--- 3 files changed, 29 insertions(+), 5 deletions(-) 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 927de19b4..6adc4c95b 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 @@ -2,12 +2,20 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; import javax.servlet.http.HttpServletRequest; +import org.apache.http.client.utils.DateUtils; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; @@ -49,7 +57,15 @@ public ResponseEntity getOne(final @PathVariable String entityId, HttpServlet return ResponseEntity.notFound().build(); } EntityDescriptorRepresentation entityDescriptorRepresentation = entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor); - return ResponseEntity.ok(entityDescriptorRepresentation); + HttpHeaders headers = new HttpHeaders(); + headers.set("Last-Modified", formatModifiedDate(entityDescriptorRepresentation)); + return new ResponseEntity<>(entityDescriptorRepresentation, headers, HttpStatus.OK); + } + + private String formatModifiedDate(EntityDescriptorRepresentation entityDescriptorRepresentation) { + Instant instant = entityDescriptorRepresentation.getModifiedDateAsDate().toInstant(ZoneOffset.UTC); + Date date = Date.from(instant); + return DateUtils.formatDate(date, DateUtils.PATTERN_RFC1123); } @RequestMapping(value = "/{entityId:.*}", produces = "application/xml") @@ -60,7 +76,10 @@ public ResponseEntity getOneXml(final @PathVariable String entityId) throws M return ResponseEntity.notFound().build(); } final String xml = this.openSamlObjects.marshalToXmlString(entityDescriptor); - return ResponseEntity.ok(xml); + EntityDescriptorRepresentation entityDescriptorRepresentation = entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor); + HttpHeaders headers = new HttpHeaders(); + headers.set("Last-Modified", formatModifiedDate(entityDescriptorRepresentation)); + return new ResponseEntity<>(xml, headers, HttpStatus.OK); } private EntityDescriptor getEntityDescriptor(final String entityId) throws ResolverException, UnsupportedEncodingException { diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java index 2131696c4..c5e89e3e7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -188,6 +188,11 @@ public void setCreatedDate(LocalDateTime createdDate) { public String getModifiedDate() { return modifiedDate != null ? modifiedDate.toString() : null; } + + public LocalDateTime getModifiedDateAsDate() { + // we shouldn't have an ED without either modified or created date, so this is mostly for testing where data can be odd + return modifiedDate != null ? modifiedDate : createdDate != null ? createdDate : LocalDateTime.now(); + } public void setModifiedDate(LocalDateTime modifiedDate) { this.modifiedDate = modifiedDate; 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 48e3bc8ae..8f262b6a1 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 @@ -139,9 +139,9 @@ class EntitiesControllerTests extends Specification { 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.CACHE_CONTROL)) // SHOULD HAVE - should be included by Spring Security // .andExpect(header().exists(HttpHeaders.ETAG)) // MUST HAVE - is done by filter, so skipped for test + .andExpect(header().exists(HttpHeaders.LAST_MODIFIED)) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(expectedBody, false)) } @@ -184,8 +184,8 @@ class EntitiesControllerTests extends Specification { .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(header().exists(HttpHeaders.LAST_MODIFIED)) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(expectedBody, false)) } From 743954c02003557fbc5aa6faa9ca696b91d486e9 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 19 May 2021 10:22:58 -0700 Subject: [PATCH 16/16] SHIBUI-1880 Annotation so that new accessor to LocalDateTime for modified date doesn't get added to JSON/XML representations (it already is in string form) --- .../ui/domain/frontend/EntityDescriptorRepresentation.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java index c5e89e3e7..32063271e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.frontend; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -189,6 +190,7 @@ public String getModifiedDate() { return modifiedDate != null ? modifiedDate.toString() : null; } + @JsonIgnore public LocalDateTime getModifiedDateAsDate() { // we shouldn't have an ED without either modified or created date, so this is mostly for testing where data can be odd return modifiedDate != null ? modifiedDate : createdDate != null ? createdDate : LocalDateTime.now();