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 d64fa8861..2843711c0 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,183 +1,237 @@ package edu.internet2.tier.shibboleth.admin.ui.controller +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository -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.ChainingMetadataResolver -import org.opensaml.saml.metadata.resolver.MetadataResolver -import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain -import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver -import org.spockframework.spring.SpringBean +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.core.io.ClassPathResource -import org.springframework.test.context.ActiveProfiles -import org.springframework.test.web.reactive.server.WebTestClient -import org.xmlunit.builder.DiffBuilder -import org.xmlunit.builder.Input -import org.xmlunit.diff.DefaultNodeMatcher -import org.xmlunit.diff.ElementSelectors -import spock.lang.Specification - -import java.time.Instant - -/** - * @author Bill Smith (wsmith@unicon.net) - */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("no-auth") -class EntitiesControllerIntegrationTests extends Specification { +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.client.RestTemplate +import spock.lang.Subject +import javax.persistence.EntityManager + +import static org.springframework.http.MediaType.APPLICATION_XML +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.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class EntitiesControllerIntegrationTests extends AbstractBaseDataJpaTest { @Autowired - private WebTestClient webClient + EntityDescriptorRepository entityDescriptorRepository - def openSamlObjects = new OpenSamlObjects().with { - init() - it - } + @Autowired + EntityManager entityManager - def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) + @Autowired + EntityService entityService + + @Autowired + TestObjectGenerator generator + + @Autowired + ObjectMapper mapper + + @Autowired + OpenSamlObjects openSamlObjects + + @Autowired + JPAEntityDescriptorServiceImpl jpaEntityDescriptorService - def metadataResolver = new ResourceBackedMetadataResolver(resource).with { - it.id = 'test' - it.parserPool = openSamlObjects.parserPool - initialize() - it + RandomGenerator randomGenerator + def mockRestTemplate = Mock(RestTemplate) + def mockMvc + + @Subject + def controller + + EntityDescriptorVersionService versionService = Mock() + + @Transactional + def setup() { + openSamlObjects.init() + + Group gb = new Group() + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + gb = groupService.createGroup(gb) + + randomGenerator = new RandomGenerator() + + controller = new EntitiesController() + controller.openSamlObjects = openSamlObjects + controller.entityDescriptorService = jpaEntityDescriptorService + controller.entityDescriptorRepository = entityDescriptorRepository + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) + userService.save(user) + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + } + + @WithMockAdmin + def 'GET /entities/{resourceId} non-existent'() { + expect: + try { + mockMvc.perform(get("/api/entities/uuid-1")) + } + catch (Exception e) { + e instanceof PersistentEntityNotFound + } } - - // 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"))) + + @WithMockAdmin + def 'GET /entities/{resourceId} existing'() { + given: + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() + entityManager.clear() + + when: + def result = mockMvc.perform(get("/api/entities/eid1")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.entityId").value("eid1")) + .andExpect(jsonPath("\$.serviceProviderName").value("sp1")) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) } - //todo review - def "GET /api/entities returns the proper json"() { + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing, validate group access'() { given: - def expectedBody = ''' - { - "entityId":"http://test.scaldingspoon.org/test1", - "serviceProviderSsoDescriptor": { - "protocolSupportEnum":"SAML 2", - "nameIdFormats":["urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"] - }, - "assertionConsumerServices":[ - {"locationUrl":"https://test.scaldingspoon.org/test1/acs","binding":"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST","makeDefault":false} - ], - "serviceEnabled":false, - "attributeRelease":["givenName","employeeNumber"] - } - ''' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "someUser") + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) + + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) + ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) when: - def result = this.webClient - .get() - .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") - .exchange() // someday, I'd like to know why IntelliJ "cannot resolve symbol 'exchange'" + def result = mockMvc.perform(get("/api/entities/eid1")) then: - result.expectStatus().isOk() - .expectBody() - .json(expectedBody) + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.entityId").value("eid1")) + .andExpect(jsonPath("\$.serviceProviderName").value("sp1")) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + .andExpect(jsonPath("\$.idOfOwner").value("someUser")) } - def "GET /api/entities/test is not found"() { + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing, owned by some other user'() { when: - def result = this.webClient - .get() - .uri("/api/entities/test") - .exchange() + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) + + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) + ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) then: - result.expectStatus().isNotFound() + try { + mockMvc.perform(get("/api/entities/eid2")) + } + catch (Exception e) { + e instanceof ForbiddenException + } } - def "GET /api/entities/test XML is not found"() { + @WithMockAdmin + def 'GET /entities/{resourceId} existing (xml)'() { + given: + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() + + def expectedXML = """ +""" + when: - def result = this.webClient - .get() - .uri("/api/entities/test") - .header('Accept', 'application/xml') - .exchange() + def result = mockMvc.perform(get("/api/entities/eid1").accept(APPLICATION_XML)) then: - result.expectStatus().isNotFound() + result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) } - def "GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML returns proper XML"() { + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing (xml), user-owned'() { given: - def expectedBody = ''' - - - - - internal - - - givenName - employeeNumber - - - - - urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified - - - -''' + Group g = userService.getCurrentUserGroup() + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorOne = entityDescriptorRepository.saveAndFlush(entityDescriptorOne) + ownershipRepository.saveAndFlush(new Ownership(g,entityDescriptorOne)) + + def expectedXML = """ +""" + when: - def result = this.webClient - .get() - .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") - .header('Accept', 'application/xml') - .exchange() + def result = mockMvc.perform(get("/api/entities/eid1").accept(APPLICATION_XML)) then: - def resultBody = result.expectStatus().isOk() - //.expectHeader().contentType("application/xml;charset=ISO-8859-1") // should this really be ISO-8859-1? - // expectedBody encoding is UTF-8... - .expectHeader().contentType("application/xml;charset=UTF-8") - .expectBody(String.class) - .returnResult() - def diff = DiffBuilder.compare(Input.fromString(expectedBody)).withTest(Input.fromString(resultBody.getResponseBody())).ignoreComments().checkForSimilar().ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)).build() - !diff.hasDifferences() + result.andExpect(status().isOk()).andExpect(content().xml(expectedXML)) } + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'GET /entities/{resourceId} existing (xml), other user-owned'() { + when: + Group g = Group.ADMIN_GROUP + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + entityDescriptorOne.setElementLocalName("EntityDescriptor") + entityDescriptorOne.setNamespacePrefix("md") + entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + entityDescriptorRepository.save(entityDescriptorOne) + entityManager.flush() - @TestConfiguration - static class Config { - @Autowired - OpenSamlObjects openSamlObjects - - @Bean - MetadataResolver metadataResolver() { - def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) - def aggregate = new ResourceBackedMetadataResolver(resource){ - @Override - Instant getLastRefresh() { - return null - } - } - - aggregate.with { - it.metadataFilter = new MetadataFilterChain() - it.id = 'testme' - it.parserPool = openSamlObjects.parserPool - it.initialize() - it - } - - return new ChainingMetadataResolver().with { - it.id = 'chain' - it.resolvers = [aggregate] - it.initialize() - it - } + then: + try { + mockMvc.perform(get("/api/entities/eid1").accept(APPLICATION_XML)) + } + catch (Exception e) { + e instanceof ForbiddenException } } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy deleted file mode 100644 index 4c572e2ad..000000000 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/AuxiliaryIntegrationTests.groovy +++ /dev/null @@ -1,59 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.service - -import com.fasterxml.jackson.databind.ObjectMapper -import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponentsConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor -import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup -import edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects -import edu.internet2.tier.shibboleth.admin.ui.security.model.Group - -import org.springframework.core.io.DefaultResourceLoader -import org.springframework.core.io.ResourceLoader -import org.springframework.mock.http.MockHttpInputMessage -import spock.lang.Shared -import spock.lang.Specification - -import java.time.LocalDateTime - -class AuxiliaryIntegrationTests extends Specification { - OpenSamlObjects openSamlObjects = new OpenSamlObjects().with { - it.init() - it - } - - JPAEntityDescriptorServiceImpl entityDescriptorService - ObjectMapper objectMapper - ResourceLoader resourceLoader - - void setup() { - entityDescriptorService = new JPAEntityDescriptorServiceImpl() - entityDescriptorService.openSamlObjects = openSamlObjects - objectMapper = new ObjectMapper() - resourceLoader = new DefaultResourceLoader() - } - - def "SHIBUI-1723: after enabling saved entity descriptor, it should still have valid xml"() { - given: - def entityDescriptor = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes) as EntityDescriptor - entityDescriptor.idOfOwner = "foo" - - def entityDescriptorRepresentation = entityDescriptorService.createRepresentationFromDescriptor(entityDescriptor).with { - it.serviceProviderName = 'testme' - it.contacts = [] - it.securityInfo.x509Certificates[0].name = 'testcert' - it.createdBy = 'root' - it.setCreatedDate(LocalDateTime.now()) - it.setModifiedDate(LocalDateTime.now()) - it - } - def json = objectMapper.writeValueAsString(entityDescriptorRepresentation) - def schemaUri = JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(this.resourceLoader, this.objectMapper)).uri - - when: - LowLevelJsonSchemaValidator.validatePayloadAgainstSchema(new MockHttpInputMessage(json.bytes), schemaUri) - - then: - noExceptionThrown() - } -} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index e9a9aa217..9e65bf28a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.configuration.JsonSchemaComponentsConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation @@ -11,6 +12,8 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup +import edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -19,12 +22,16 @@ import org.skyscreamer.jsonassert.JSONAssert import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.json.JacksonTester import org.springframework.context.annotation.PropertySource +import org.springframework.core.io.DefaultResourceLoader +import org.springframework.mock.http.MockHttpInputMessage import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.diff.DefaultNodeMatcher import org.xmlunit.diff.ElementSelectors import spock.lang.Ignore +import java.time.LocalDateTime + @PropertySource("classpath:application.yml") class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { @Autowired @@ -763,4 +770,29 @@ class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { return ed } + + def "SHIBUI-1723"() { + given: + def entityDescriptor = openSamlObjects.unmarshalFromXml(this.class.getResource('/metadata/SHIBUI-1723-1.xml').bytes) as EntityDescriptor + entityDescriptor.idOfOwner = "foo" + + def entityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptor).with { + it.serviceProviderName = 'testme' + it.contacts = [] + it.securityInfo.x509Certificates[0].name = 'testcert' + it.createdBy = 'root' + it.setCreatedDate(LocalDateTime.now()) + it.setModifiedDate(LocalDateTime.now()) + it + } + def json = mapper.writeValueAsString(entityDescriptorRepresentation) + def resourceLoader = new DefaultResourceLoader() + def schemaUri = JsonSchemaLocationLookup.metadataSourcesSchema(new JsonSchemaComponentsConfiguration().jsonSchemaResourceLocationRegistry(resourceLoader, this.mapper)).uri + + when: + LowLevelJsonSchemaValidator.validatePayloadAgainstSchema(new MockHttpInputMessage(json.bytes), schemaUri) + + then: + noExceptionThrown() + } } \ No newline at end of file