diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index f8e28fc75..ff0e3732c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; 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.model.User; @@ -73,7 +74,8 @@ public EntityDescriptorController(EntityDescriptorVersionService versionService) @PostMapping("/EntityDescriptor") @Transactional - public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException { + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } @@ -145,7 +147,8 @@ public void initRestTemplate() { @PutMapping("/EntityDescriptor/{resourceId}") @Transactional - public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException { + public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) + throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException, InvalidUrlMatchException { edRepresentation.setId(resourceId); // This should be the same already, but just to be safe... EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation); return ResponseEntity.ok().body(result); @@ -171,4 +174,4 @@ public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam .body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage())); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java index d0b18f415..7eae86cb5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java @@ -1,7 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import java.util.ConcurrentModificationException; - +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,9 +12,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; -import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import java.util.ConcurrentModificationException; @ControllerAdvice(assignableTypes = {EntityDescriptorController.class}) public class EntityDescriptorControllerExceptionHandler extends ResponseEntityExceptionHandler { @@ -21,24 +21,30 @@ public class EntityDescriptorControllerExceptionHandler extends ResponseEntityEx public ResponseEntity handleConcurrentModificationException(ConcurrentModificationException e, WebRequest request) { return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); } - + @ExceptionHandler({ EntityIdExistsException.class }) public ResponseEntity handleEntityExistsException(EntityIdExistsException e, WebRequest request) { HttpHeaders headers = new HttpHeaders(); headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage())); - return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse( - String.valueOf(HttpStatus.CONFLICT.value()), - String.format("The entity descriptor with entity id [%s] already exists.", e.getMessage()))); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), + String.format("The entity descriptor with entity id [%s] already exists.", + e.getMessage()))); } - + @ExceptionHandler({ EntityNotFoundException.class }) public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); } - + @ExceptionHandler({ ForbiddenException.class }) public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage())); } -} + + @ExceptionHandler({ InvalidUrlMatchException.class }) + public ResponseEntity handleInvalidUrlMatchException(InvalidUrlMatchException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage())); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java new file mode 100644 index 000000000..ea2d463c7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InvalidUrlMatchException extends Exception { + public InvalidUrlMatchException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java index 7f461c816..bcca4c088 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java @@ -56,6 +56,16 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException, groupRepository.delete(group); } + /** + * Though the name URI is used here, any string value that we want to validate against the group's regex is accepted and checked. + * Designed usage is that this would be a URL or an entity Id (which is a URI that does not have to follow the URL conventions) + */ + @Override + public boolean doesUrlMatchGroupPattern(String groupId, String uri) { + Group group = find(groupId); + return Pattern.matches(group.getValidationRegex(), uri); + } + @Override @Transactional public void ensureAdminGroupExists() { @@ -64,7 +74,7 @@ public void ensureAdminGroupExists() { g = new Group(); g.setName("ADMIN-GROUP"); g.setResourceId("admingroup"); - g.setValidationRegex("/*"); // Everything + g.setValidationRegex("^.+$"); // Just about everything g = groupRepository.save(g); } Group.ADMIN_GROUP = g; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java index 95314123f..d95415127 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java @@ -23,4 +23,5 @@ public interface IGroupService { Group updateGroup(Group g) throws EntityNotFoundException, InvalidGroupRegexException; + boolean doesUrlMatchGroupPattern(String groupId, String uri); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java index bc63e7378..54fe65ddd 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityDescriptorService.java @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; import java.util.ConcurrentModificationException; import java.util.List; @@ -31,7 +32,8 @@ public interface EntityDescriptorService { * @throws ForbiddenException If user is unauthorized to perform this operation * @throws EntityIdExistsException If any EntityDescriptor already exists with the same EntityId */ - EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptor ed) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException; /** * @param edRepresentation Incoming representation to save @@ -39,7 +41,8 @@ public interface EntityDescriptorService { * @throws ForbiddenException If user is unauthorized to perform this operation * @throws EntityIdExistsException If the entity already exists */ - EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException; /** * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor @@ -93,13 +96,13 @@ public interface EntityDescriptorService { Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); /** - * @param edRepresentation Incoming representation to save - * @return EntityDescriptorRepresentation - * @throws ForbiddenException If user is unauthorized to perform this operation - * @throws EntityIdExistsException If the entity already exists - * @throws ConcurrentModificationException If the entity was already modified by another user + * @throws ForbiddenException If the user is not permitted to perform the action + * @throws EntityNotFoundException If the entity doesn't already exist in the database + * @throws ConcurrentModificationException IF the entity is being modified in another session + * @throws InvalidUrlMatchException If the entity id or the ACS location urls don't match the supplied regex */ - EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException; + EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException, InvalidUrlMatchException; /** * Update an instance of entity descriptor with information from the front-end representation diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 9e0f7117e..84c956d93 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -1,24 +1,11 @@ package edu.internet2.tier.shibboleth.admin.ui.service; -import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributes; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; -import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation; -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.domain.*; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.*; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; 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.model.Group; @@ -29,23 +16,14 @@ import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; import lombok.extern.slf4j.Slf4j; - -import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; -import javax.transaction.Transactional; - import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.*; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; @Slf4j @Service @@ -94,12 +72,14 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptor ed) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { return createNew(createRepresentationFromDescriptor(ed)); } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } @@ -107,9 +87,15 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { throw new EntityIdExistsException(edRep.getEntityId()); } - + + // "Create new" will use the current user's group as the owner + String ownerId = userService.getCurrentUserGroup().getOwnerId(); + edRep.setIdOfOwner(ownerId); + validateEntityIdAndACSUrls(edRep); + EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); - ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); + ed.setIdOfOwner(ownerId); + return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } @@ -331,14 +317,14 @@ public void delete(String resourceId) throws ForbiddenException, EntityNotFoundE } ownershipRepository.deleteEntriesForOwnedObject(ed); entityDescriptorRepository.delete(ed); - + } @Override public Iterable getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException { if (!userService.currentUserIsAdmin()) { throw new ForbiddenException(); - } + } return entityDescriptorRepository.findAllDisabledAndNotOwnedByAdmin().map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); } @@ -371,31 +357,34 @@ public EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throw } if (!userService.isAuthorizedFor(ed)) { throw new ForbiddenException(); - } + } return ed; } - + @Override public Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { return ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList(attributeList); } - + @Override - public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityNotFoundException { + public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) + throws ForbiddenException, EntityNotFoundException, InvalidUrlMatchException { EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId()); if (existingEd == null) { - throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); + throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); } if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } if (!userService.isAuthorizedFor(existingEd)) { throw new ForbiddenException(); - } + } // Verify we're the only one attempting to update the EntityDescriptor if (edRep.getVersion() != existingEd.hashCode()) { - throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId())); + throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId())); } + + validateEntityIdAndACSUrls(edRep); updateDescriptorFromRepresentation(existingEd, edRep); return createRepresentationFromDescriptor(entityDescriptorRepository.save(existingEd)); } @@ -407,4 +396,21 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. } buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); } -} + + private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidUrlMatchException { + // Check the entity id first + if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { + throw new InvalidUrlMatchException("EntityId is not a pattern match to the group"); + } + + // Check the ACS locations + if (edRep.getAssertionConsumerServices() != null && edRep.getAssertionConsumerServices().size() > 0) { + for (AssertionConsumerServiceRepresentation acs : edRep.getAssertionConsumerServices()) { + if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), acs.getLocationUrl())) { + throw new InvalidUrlMatchException( + "ACS location [ " + acs.getLocationUrl() + " ] is not a pattern match to the group"); + } + } + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index a04c9ebba..717aeb155 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -6,9 +6,12 @@ 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.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException 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.model.Group @@ -65,7 +68,7 @@ import org.springframework.web.client.RestTemplate import org.springframework.web.servlet.config.annotation.EnableWebMvc import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - +import org.springframework.web.util.NestedServletException import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification @@ -137,7 +140,15 @@ class EntityDescriptorControllerTests extends Specification { @Transactional def setup() { + groupService.clearAllForTesting() groupService.ensureAdminGroupExists() + + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + gb = groupService.createGroup(gb) + generator = new TestObjectGenerator() randomGenerator = new RandomGenerator() mapper = new ObjectMapper() @@ -177,6 +188,7 @@ class EntityDescriptorControllerTests extends Specification { 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) @@ -190,22 +202,22 @@ class EntityDescriptorControllerTests extends Specification { authentication.getName() >> 'admin' def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false) entityDescriptorRepository.save(entityDescriptor) - + when: 'pre-check' entityManager.flush() - + then: entityDescriptorRepository.findAll().size() == 1 - + when: def result = mockMvc.perform(delete("/api/EntityDescriptor/uuid-1")) - + then: result.andExpect(status().isNoContent()) entityDescriptorRepository.findByResourceId("uuid-1") == null entityDescriptorRepository.findAll().size() == 0 } - + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptors with empty repository as admin'() { @@ -230,17 +242,17 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptors with 1 record in repository as admin'() { given: authentication.getName() >> 'admin' - - def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + + def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") entityDescriptorRepository.saveAndFlush(entityDescriptor) - + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() - when: + when: def result = mockMvc.perform(get('/api/EntityDescriptors')) - then: + then: def mvcResult = result.andExpect(expectedHttpResponseStatus).andExpect(content().contentType(expectedResponseContentType)) .andExpect(jsonPath("\$.[0].id").value("uuid-1")) .andExpect(jsonPath("\$.[0].entityId").value("eid1")) @@ -253,14 +265,14 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptors with 2 records in repository as admin'() { given: authentication.getName() >> 'admin' - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: "admingroup") - + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) - + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() @@ -278,20 +290,73 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.[1].entityId").value("eid2")) .andExpect(jsonPath("\$.[1].serviceEnabled").value(false)) .andExpect(jsonPath("\$.[1].idOfOwner").value("admingroup")) - } - + } + + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'POST create new - verifying validation on entityID and ACS locations'() { + given: + authentication.getName() >> 'someUser' + + def expectedEntityId = 'https://shib.org/blah/blah' + EntityDescriptorRepresentation edRep = new EntityDescriptorRepresentation() + edRep.setEntityId(expectedEntityId) + edRep.setServiceProviderName("spName") + + def acsList = new ArrayList() + AssertionConsumerServiceRepresentation acsRep = new AssertionConsumerServiceRepresentation() + acsRep.setIndex(0) + acsRep.setLocationUrl("http://logout.shib.org/dologout") + acsList.add(acsRep) + edRep.setAssertionConsumerServices(acsList) + + def edRepJson = mapper.writeValueAsString(edRep) + + when: + def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.entityId").value("https://shib.org/blah/blah")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("testingGroupBBB")) + + when: "ACS url is bad" + expectedEntityId = 'https://shib.org/blah/blah/again' + edRep = new EntityDescriptorRepresentation() + edRep.setEntityId(expectedEntityId) + edRep.setServiceProviderName("spName") + + acsList = new ArrayList() + acsRep = new AssertionConsumerServiceRepresentation() + acsRep.setIndex(0) + acsRep.setLocationUrl("http://shib.com/dologout") + acsList.add(acsRep) + edRep.setAssertionConsumerServices(acsList) + edRepJson = mapper.writeValueAsString(edRep) + + then: + try { + mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof InvalidUrlMatchException + } + } + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST /EntityDescriptor and successfully create new record'() { given: - authentication.getName() >> 'admin' - + authentication.getName() >> 'admin' + def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' def expectedResponseHeader = 'Location' def expectedResponseHeaderValue = "/api/EntityDescriptor/" - def postedJsonBody = """ + def postedJsonBody = """ { "serviceProviderName": "$expectedSpName", "entityId": "$expectedEntityId", @@ -305,7 +370,7 @@ class EntityDescriptorControllerTests extends Specification { "securityInfo": null, "assertionConsumerServices": null, "current": false - } + } """ when: @@ -324,13 +389,13 @@ class EntityDescriptorControllerTests extends Specification { def 'POST /EntityDescriptor as user disallows enabling'() { given: authentication.getName() >> 'someUser' - + def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' when: def postedJsonBody = """ - { + { "serviceProviderName": "$expectedSpName", "entityId": "$expectedEntityId", "organization": null, @@ -346,9 +411,9 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null - } + } """ - + then: try { def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -364,7 +429,7 @@ class EntityDescriptorControllerTests extends Specification { given: authentication.getName() >> 'admin' - def postedJsonBody = """ + def postedJsonBody = """ { "serviceProviderName": "sp1", "entityId": "eid1", @@ -381,17 +446,17 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null - } + } """ when: def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false) - + entityDescriptorRepository.save(entityDescriptorOne) entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() - + then: try { def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -400,7 +465,7 @@ class EntityDescriptorControllerTests extends Specification { e instanceof EntityIdExistsException == true } } - + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptor/{resourceId} non-existent'() { @@ -415,7 +480,7 @@ class EntityDescriptorControllerTests extends Specification { e instanceof EntityNotFoundException == true } } - + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptor/{resourceId} existing'() { @@ -424,10 +489,10 @@ class EntityDescriptorControllerTests extends Specification { def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() - + when: - def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) - + def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) + then: result.andExpect(status().isOk()) .andExpect(jsonPath("\$.entityId").value("eid1")) @@ -442,17 +507,17 @@ class EntityDescriptorControllerTests extends Specification { given: authentication.getName() >> 'someUser' 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 = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) @@ -470,13 +535,13 @@ class EntityDescriptorControllerTests extends Specification { when: authentication.getName() >> 'someUser' 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)) @@ -486,7 +551,7 @@ class EntityDescriptorControllerTests extends Specification { } catch (Exception e) { e instanceof ForbiddenException == true - } + } } @Rollback @@ -494,7 +559,7 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing (xml)'() { given: authentication.getName() >> 'admin' - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + 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") @@ -518,7 +583,7 @@ class EntityDescriptorControllerTests extends Specification { given: authentication.getName() >> 'someUser' 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") @@ -543,14 +608,14 @@ class EntityDescriptorControllerTests extends Specification { when: authentication.getName() >> 'someUser' 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() - + then: try { def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId").accept(APPLICATION_XML)) @@ -565,7 +630,7 @@ class EntityDescriptorControllerTests extends Specification { def "POST /EntityDescriptor handles XML happily"() { given: authentication.getName() >> 'admin' - + def postedBody = ''' @@ -599,7 +664,7 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.protocolSupportEnum").value("SAML 2")) - .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.nameIdFormats[0]").value("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")) + .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.nameIdFormats[0]").value("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")) .andExpect(jsonPath("\$.assertionConsumerServices[0].binding").value("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")) .andExpect(jsonPath("\$.assertionConsumerServices[0].makeDefault").value(false)) .andExpect(jsonPath("\$.assertionConsumerServices[0].locationUrl").value("https://test.scaldingspoon.org/test1/acs")) @@ -633,11 +698,11 @@ class EntityDescriptorControllerTests extends Specification { def spName = randomGenerator.randomString() def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'http://test.scaldingspoon.org/test1', serviceProviderName: 'sp2', serviceEnabled: false) - + entityDescriptorRepository.save(entityDescriptorOne) entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() - + then: try { def exceptionExpected = mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) @@ -652,9 +717,9 @@ class EntityDescriptorControllerTests extends Specification { def "PUT /EntityDescriptor updates entity descriptors properly as admin"() { given: authentication.getName() >> 'admin' - + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) - + entityDescriptorTwo = entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() entityManager.clear() @@ -662,7 +727,7 @@ class EntityDescriptorControllerTests extends Specification { def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorTwo) updatedEntityDescriptorRepresentation.setServiceProviderName("newName") def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) - + when: def result = mockMvc.perform(put("/api/EntityDescriptor/uuid-2").contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -676,12 +741,12 @@ class EntityDescriptorControllerTests extends Specification { @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) - def "PUT /EntityDescriptor disallows non-admin user from enabling"() { + def "PUT /EntityDescriptor disallows non-admin user from enabling"() { given: authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: g.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: g.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() @@ -703,12 +768,12 @@ class EntityDescriptorControllerTests extends Specification { @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) - def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { + def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { given: authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() @@ -733,8 +798,8 @@ class EntityDescriptorControllerTests extends Specification { def "PUT /EntityDescriptor throws a concurrent mod exception if the version numbers don't match"() { given: authentication.getName() >> 'admin' - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() @@ -742,7 +807,7 @@ class EntityDescriptorControllerTests extends Specification { entityDescriptorOne.serviceProviderName = 'foo' entityDescriptorOne.resourceId = 'uuid-1' def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) - + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) then: @@ -768,13 +833,4 @@ class EntityDescriptorControllerTests extends Specification { return result } } -} - -//when: -//def Set ownerships = ownershipRepository.findOwnableObjectOwners(ed) -// -//then: -//ownerships.size() == 1 -//ownerships.each { -// it.ownerId == groupFromDb.resourceId -//} \ No newline at end of file +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy index 13847472c..906bc4a19 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy @@ -63,11 +63,12 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { Group ga = new Group() ga.setResourceId("testingGroup") ga.setName("Group A") - ga = groupService.createGroup(ga) + groupService.createGroup(ga) - Group gb = new Group(); + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") gb = groupService.createGroup(gb) def roles = [new Role().with { @@ -100,12 +101,9 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { User current = userService.getCurrentUser() current.setGroupId("testingGroupBBB") - def expectedCreationDate = '2017-10-23T11:11:11' - def expectedEntityId = 'https://shib' + def expectedEntityId = 'https://shib.org/blah' def expectedSpName = 'sp1' def expectedUUID = 'uuid-1' - def expectedResponseHeader = 'Location' - def expectedResponseHeaderValue = "/api/EntityDescriptor/$expectedUUID" def entityDescriptor = new EntityDescriptor(resourceId: expectedUUID, entityID: expectedEntityId, serviceProviderName: expectedSpName, serviceEnabled: false) when: @@ -128,4 +126,4 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { return result } } -} +} \ No newline at end of file