From c356817ae387e763688fb06bbe7ec20d8de6a61c Mon Sep 17 00:00:00 2001 From: chasegawa Date: Sat, 3 Jul 2021 00:28:42 -0700 Subject: [PATCH] SHIBUI-1848 Refactoring EntityDescriptorController - moving as much logic for permission checking and error handling out of the controller to the service layer and ExceptionHandler --- .../EntityDescriptorController.java | 114 +++++------------- ...yDescriptorControllerExceptionHandler.java | 44 +++++++ .../ui/exception/EntityIdExistsException.java | 8 ++ .../ui/exception/EntityNotFoundException.java | 7 ++ .../ui/exception/ForbiddenException.java | 11 ++ .../ui/security/service/UserService.java | 19 ++- .../ui/service/EntityDescriptorService.java | 14 ++- .../JPAEntityDescriptorServiceImpl.java | 60 ++++++++- .../EntityDescriptorControllerTests.groovy | 75 ++++++------ 9 files changed, 224 insertions(+), 128 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java 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 f952b2a4d..a7e74947f 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 @@ -3,12 +3,16 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; +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.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService; import lombok.extern.slf4j.Slf4j; import org.opensaml.core.xml.io.MarshallingException; @@ -20,6 +24,7 @@ import org.springframework.security.access.annotation.Secured; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -32,7 +37,9 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import javax.annotation.PostConstruct; + import java.net.URI; +import java.util.ConcurrentModificationException; import java.util.List; import java.util.stream.Collectors; @@ -40,14 +47,14 @@ @RequestMapping("/api") @Slf4j public class EntityDescriptorController { - private static URI getResourceUriFor(EntityDescriptor ed) { + static URI getResourceUriFor(String resourceId) { return ServletUriComponentsBuilder .fromCurrentServletMapping().path("/api/EntityDescriptor") - .pathSegment(ed.getResourceId()) + .pathSegment(resourceId) .build() .toUri(); } - + @Autowired private EntityDescriptorRepository entityDescriptorRepository; @@ -73,58 +80,28 @@ public EntityDescriptorController(UserService userService, EntityDescriptorVersi @PostMapping("/EntityDescriptor") @Transactional - public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) { - final String entityId = edRepresentation.getEntityId(); - - ResponseEntity entityDescriptorEnablingDeniedResponse = entityDescriptorEnablePermissionsCheck(edRepresentation.isServiceEnabled()); - if (entityDescriptorEnablingDeniedResponse != null) { - return entityDescriptorEnablingDeniedResponse; - } - - ResponseEntity existingEntityDescriptorConflictResponse = existingEntityDescriptorCheck(entityId); - if (existingEntityDescriptorConflictResponse != null) { - return existingEntityDescriptorConflictResponse; - } - - EntityDescriptor ed = (EntityDescriptor) entityDescriptorService.createDescriptorFromRepresentation(edRepresentation); - - EntityDescriptor persistedEd = entityDescriptorRepository.save(ed); - edRepresentation.setId(persistedEd.getResourceId()); - edRepresentation.setCreatedDate(persistedEd.getCreatedDate()); - return ResponseEntity.created(getResourceUriFor(persistedEd)).body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException { + EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); + return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } @Secured("ROLE_ADMIN") @DeleteMapping(value = "/EntityDescriptor/{resourceId}") @Transactional - public ResponseEntity deleteOne(@PathVariable String resourceId) { - EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); - if (ed == null) { - return ResponseEntity.notFound().build(); - } else if (ed.isServiceEnabled()) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, "Deleting an enabled Metadata Source is not allowed. Disable the source and try again.")); - } else { - entityDescriptorRepository.delete(ed); - return ResponseEntity.noContent().build(); - } - } - - private ResponseEntity entityDescriptorEnablePermissionsCheck(boolean serviceEnabled) { - User user = userService.getCurrentUser(); - if (user != null) { - if (serviceEnabled && !user.getRole().equals("ROLE_ADMIN")) { - return ResponseEntity.status(HttpStatus.FORBIDDEN) - .body(new ErrorResponse(HttpStatus.FORBIDDEN, "You do not have the permissions necessary to enable this service.")); - } + public ResponseEntity deleteOne(@PathVariable String resourceId) throws ForbiddenException, EntityNotFoundException { + EntityDescriptor ed = entityDescriptorService.getEntityDescriptorByResourceId(resourceId); + if (ed.isServiceEnabled()) { + throw new ForbiddenException("Deleting an enabled Metadata Source is not allowed. Disable the source and try again."); } - return null; + entityDescriptorRepository.delete(ed); + return ResponseEntity.noContent().build(); } private ResponseEntity existingEntityDescriptorCheck(String entityId) { final EntityDescriptor ed = entityDescriptorRepository.findByEntityID(entityId); if (ed != null) { HttpHeaders headers = new HttpHeaders(); - headers.setLocation(getResourceUriFor(ed)); + headers.setLocation(getResourceUriFor(ed.getResourceId())); return ResponseEntity .status(HttpStatus.CONFLICT) .headers(headers) @@ -137,12 +114,10 @@ private ResponseEntity existingEntityDescriptorCheck(String entityId) { @GetMapping("/EntityDescriptors") @Transactional(readOnly = true) public ResponseEntity getAll() { - User currentUser = userService.getCurrentUser(); - if (currentUser != null) { + try { return ResponseEntity.ok(entityDescriptorService.getAllRepresentationsBasedOnUserAccess()); - } else { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, - "You are not authorized to perform the requested operation.")); + } catch (ForbiddenException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage())); } } @@ -225,6 +200,11 @@ public ResponseEntity getSpecificVersion(@PathVariable String resourceId, @Pa return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } + @ExceptionHandler({ ForbiddenException.class }) + public void handleException() { + // + } + private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, String spName) throws Exception { final EntityDescriptor ed = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(rawXmlBytes)); @@ -235,7 +215,7 @@ private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, ed.setServiceProviderName(spName); final EntityDescriptor persistedEd = entityDescriptorRepository.save(ed); - return ResponseEntity.created(getResourceUriFor(persistedEd)) + return ResponseEntity.created(getResourceUriFor(persistedEd.getResourceId())) .body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); } @@ -246,38 +226,10 @@ public void initRestTemplate() { @PutMapping("/EntityDescriptor/{resourceId}") @Transactional - public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) { - User currentUser = userService.getCurrentUser(); - if (currentUser != null) { - EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(resourceId); - - if (existingEd == null) { - return ResponseEntity.notFound().build(); - } else { - if (userService.isAuthorizedFor(existingEd.getCreatedBy(), - existingEd.getGroup() == null ? null : existingEd.getGroup().getResourceId())) { - if (!existingEd.isServiceEnabled()) { - ResponseEntity entityDescriptorEnablingDeniedResponse = entityDescriptorEnablePermissionsCheck( - edRepresentation.isServiceEnabled()); - if (entityDescriptorEnablingDeniedResponse != null) { - return entityDescriptorEnablingDeniedResponse; - } - } - - // Verify we're the only one attempting to update the EntityDescriptor - if (edRepresentation.getVersion() != existingEd.hashCode()) { - return new ResponseEntity(HttpStatus.CONFLICT); - } - - entityDescriptorService.updateDescriptorFromRepresentation(existingEd, edRepresentation); - existingEd = entityDescriptorRepository.save(existingEd); - - return ResponseEntity.ok().body(entityDescriptorService.createRepresentationFromDescriptor(existingEd)); - } - } - } - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, - "You are not authorized to perform the requested operation.")); + public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException { + edRepresentation.setId(resourceId); // This should be the same already, but just to be safe... + EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation); + return ResponseEntity.ok().body(result); } @PostMapping(value = "/EntityDescriptor", consumes = "application/xml") 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 new file mode 100644 index 000000000..f89c16f8c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java @@ -0,0 +1,44 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import java.util.ConcurrentModificationException; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +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; + +@ControllerAdvice(assignableTypes = {EntityDescriptorController.class}) +public class EntityDescriptorControllerExceptionHandler extends ResponseEntityExceptionHandler { + + @ExceptionHandler({ ForbiddenException.class }) + public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, 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()))); + + } + + @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({ ConcurrentModificationException.class }) + public ResponseEntity handleConcurrentModificationException(ConcurrentModificationException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java new file mode 100644 index 000000000..990eab2c3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class EntityIdExistsException extends Exception { + public EntityIdExistsException(String entityId) { + super(entityId); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java new file mode 100644 index 000000000..6769aaa5f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityNotFoundException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class EntityNotFoundException extends Exception { + public EntityNotFoundException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java new file mode 100644 index 000000000..66f2e61c5 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ForbiddenException.java @@ -0,0 +1,11 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class ForbiddenException extends Exception { + public ForbiddenException() { + super("You are not authorized to perform the requested operation."); + } + + public ForbiddenException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index 220bb090c..61684c592 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; 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.security.repository.RoleRepository; @@ -50,18 +51,25 @@ public UserAccess getCurrentUserAccess() { return UserAccess.OWNER; } } + + public boolean isAuthorizedFor(String objectCreatedBy, Group objectGroup) { + String groupId = objectGroup == null ? "" : objectGroup.getResourceId(); + return isAuthorizedFor(objectCreatedBy, groupId); + } + public boolean isAuthorizedFor(String objectCreatedBy, String objectGroupResourceId) { - User currentUser = getCurrentUser(); String groupId = objectGroupResourceId == null ? "" : objectGroupResourceId; - switch (getCurrentUserAccess()) { + switch (getCurrentUserAccess()) { // no user returns NONE case ADMIN: return true; case GROUP: + User currentUser = getCurrentUser(); return objectCreatedBy.equals(currentUser.getUsername()) || groupId.equals(currentUser.getGroupId()); case OWNER: - return objectCreatedBy.equals(currentUser.getUsername()); + User cu = getCurrentUser(); + return objectCreatedBy.equals(cu.getUsername()); default: return false; } @@ -89,4 +97,9 @@ public void updateUserRole(User user) { throw new RuntimeException(String.format("User with username [%s] has no role defined and therefor cannot be updated!", user.getUsername())); } } + + public boolean currentUserIsAdmin() { + User user = getCurrentUser(); + return user != null && user.getRole().equals("ROLE_ADMIN"); + } } \ 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 cdcf80774..5c05d43db 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 @@ -2,8 +2,13 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; 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 org.opensaml.saml.saml2.metadata.EntityDescriptor; +import java.util.ConcurrentModificationException; import java.util.List; import java.util.Map; @@ -13,7 +18,6 @@ * @since 1.0 */ public interface EntityDescriptorService { - /** * Map from front-end data representation of entity descriptor to opensaml implementation of entity descriptor model * @@ -33,7 +37,7 @@ public interface EntityDescriptorService { /** * @return a list of EntityDescriptorRepresentations that a user has the rights to access */ - List getAllRepresentationsBasedOnUserAccess(); + List getAllRepresentationsBasedOnUserAccess() throws ForbiddenException; /** * Given a list of attributes, generate an AttributeReleaseList @@ -59,4 +63,10 @@ public interface EntityDescriptorService { */ void updateDescriptorFromRepresentation(final EntityDescriptor entityDescriptor, final EntityDescriptorRepresentation representation); + EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException; + + EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException; + + edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throws EntityNotFoundException; + } \ No newline at end of file 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 5734f55ec..feffa2fc5 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 @@ -2,6 +2,8 @@ import com.google.common.base.Strings; + +import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; import edu.internet2.tier.shibboleth.admin.ui.domain.AssertionConsumerService; import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBuilder; @@ -41,12 +43,14 @@ 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.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.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; -import edu.internet2.tier.shibboleth.admin.ui.security.service.UserAccess; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; @@ -57,13 +61,14 @@ import org.opensaml.xmlsec.signature.KeyInfo; import org.opensaml.xmlsec.signature.X509Certificate; import org.opensaml.xmlsec.signature.X509Data; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -400,7 +405,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope } @Override - public List getAllRepresentationsBasedOnUserAccess() { + public List getAllRepresentationsBasedOnUserAccess() throws ForbiddenException { switch (userService.getCurrentUserAccess()) { case ADMIN: return entityDescriptorRepository.findAllStreamByCustomQuery().map(ed -> createRepresentationFromDescriptor(ed)) @@ -412,10 +417,10 @@ public List getAllRepresentationsBasedOnUserAcce .findAllStreamByGroup_resourceIdOrCreatedBy(group == null ? null : group.getResourceId(), user.getUsername()) .map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); case OWNER: - default: return entityDescriptorRepository.findAllStreamByCreatedBy(userService.getCurrentUser().getUsername()) .map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); - + default: + throw new ForbiddenException(); } } @@ -738,4 +743,47 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. } buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); } + + @Override + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException { + if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { + throw new ForbiddenException("You do not have the permissions necessary to enable this service."); + } + + if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { + throw new EntityIdExistsException(edRep.getEntityId()); + } + + EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); + return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); + } + + @Override + public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityNotFoundException { + 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())); + } + if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { + throw new ForbiddenException("You do not have the permissions necessary to enable this service."); + } + if (!userService.isAuthorizedFor(existingEd.getCreatedBy(), existingEd.getGroup())) { + throw new ForbiddenException("You are not authorized to perform the requested operation."); + } + // 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())); + } + updateDescriptorFromRepresentation(existingEd, edRep); + return createRepresentationFromDescriptor(entityDescriptorRepository.save(existingEd)); + } + + @Override + public EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throws EntityNotFoundException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found.", resourceId)); + } + return ed; + } } 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 08f42f537..fc2c2c46c 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,6 +6,8 @@ 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.exception.EntityIdExistsException +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException 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.repository.RoleRepository @@ -20,8 +22,12 @@ import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import groovy.json.JsonOutput import groovy.json.JsonSlurper + +import org.springframework.beans.factory.support.RootBeanDefinition import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.context.support.StaticApplicationContext import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContext @@ -29,6 +35,10 @@ import org.springframework.security.core.context.SecurityContextHolder import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders 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 spock.lang.Ignore import spock.lang.Specification import spock.lang.Subject @@ -92,8 +102,8 @@ class EntityDescriptorControllerTests extends Specification { controller.restTemplate = mockRestTemplate - mockMvc = MockMvcBuilders.standaloneSetup(controller).build() - + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + securityContext.getAuthentication() >> authentication SecurityContextHolder.setContext(securityContext) @@ -410,20 +420,18 @@ class EntityDescriptorControllerTests extends Specification { """ when: - def result = mockMvc.perform( - post('/api/EntityDescriptor') - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def exception = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)).andReturn().getResolvedException() then: - 0 * entityDescriptorRepository.findByEntityID(_) - 0 * entityDescriptorRepository.save(_) - - result.andExpect(status().isForbidden()) + exception instanceof ForbiddenException == true } def 'POST /EntityDescriptor record already exists'() { given: + def username = 'admin' + def role = 'ROLE_ADMIN' + authentication.getName() >> username + userRepository.findByUsername(username) >> TestHelpers.generateOptionalUser(username, role) def expectedEntityId = 'eid1' def postedJsonBody = """ { @@ -446,15 +454,16 @@ class EntityDescriptorControllerTests extends Specification { """ when: - def result = mockMvc.perform( - post('/api/EntityDescriptor') - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + //Stub invocation of the repository returning an existing record + 1 * entityDescriptorRepository.findByEntityID(expectedEntityId) >> new EntityDescriptor(entityID: expectedEntityId) then: - //Stub invocation of the repository returning an existing record - 1 * entityDescriptorRepository.findByEntityID(expectedEntityId) >> new EntityDescriptor(entityID: expectedEntityId) - result.andExpect(status().isConflict()) + try { + def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) + } + catch (Exception e) { + e instanceof EntityIdExistsException == true + } } def 'GET /EntityDescriptor/{resourceId} non-existent'() { @@ -964,13 +973,10 @@ class EntityDescriptorControllerTests extends Specification { 0 * entityDescriptorRepository.save(_) >> updatedEntityDescriptor when: - def result = mockMvc.perform( - put("/api/EntityDescriptor/$resourceId") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def exception = mockMvc.perform(put("/api/EntityDescriptor/$resourceId").contentType(APPLICATION_JSON).content(postedJsonBody)).andReturn().getResolvedException() then: - result.andExpect(status().isForbidden()) + exception instanceof ForbiddenException == true } def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { @@ -990,16 +996,13 @@ class EntityDescriptorControllerTests extends Specification { 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor when: - def result = mockMvc.perform( - put("/api/EntityDescriptor/$resourceId") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def exception = mockMvc.perform(put("/api/EntityDescriptor/$resourceId").contentType(APPLICATION_JSON).content(postedJsonBody)).andReturn().getResolvedException() then: - result.andExpect(status().is(403)) + exception instanceof ForbiddenException == true } - def "PUT /EntityDescriptor 409's if the version numbers don't match"() { + def "PUT /EntityDescriptor throws a concurrent mod exception if the version numbers don't match"() { given: def username = 'admin' def role = 'ROLE_ADMIN' @@ -1013,15 +1016,15 @@ class EntityDescriptorControllerTests extends Specification { def resourceId = entityDescriptor.resourceId - 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor - when: - def result = mockMvc.perform( - put("/api/EntityDescriptor/$resourceId") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) - + 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor + then: - result.andExpect(status().is(409)) + try { + def exception = mockMvc.perform(put("/api/EntityDescriptor/$resourceId").contentType(APPLICATION_JSON).content(postedJsonBody)) + } + catch (Exception e) { + e instanceof ConcurrentModificationException == true + } } }