diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java new file mode 100644 index 000000000..a9ac9160a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleController.java @@ -0,0 +1,60 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +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.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/custom/entity/bundles") +@Slf4j +public class AttributeBundleController { + @Autowired AttributeBundleService attributeBundleService; + + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody AttributeBundle bundle) throws ObjectIdExistsException { + AttributeBundle result = attributeBundleService.create(bundle); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException { + attributeBundleService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(attributeBundleService.findAll()); + } + + @Secured("ROLE_ADMIN") + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody AttributeBundle bundle) throws EntityNotFoundException { + AttributeBundle result = attributeBundleService.updateBundle(bundle); + return ResponseEntity.ok(result); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java new file mode 100644 index 000000000..9f5266c3c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleExceptionHandler.java @@ -0,0 +1,29 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +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; + +@ControllerAdvice(assignableTypes = {AttributeBundleController.class}) +public class AttributeBundleExceptionHandler extends ResponseEntityExceptionHandler { + @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({ ObjectIdExistsException.class }) + public ResponseEntity handleObjectIdExistsException(ObjectIdExistsException 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 attribute bundle with resource id [%s] already exists.", e.getMessage()))); + + } +} \ No newline at end of file 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..99eb079f6 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 @@ -2,14 +2,10 @@ 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.ObjectIdExistsException; 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 lombok.extern.slf4j.Slf4j; @@ -17,13 +13,10 @@ import org.opensaml.core.xml.io.MarshallingException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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; @@ -39,8 +32,6 @@ import java.net.URI; import java.util.ConcurrentModificationException; -import java.util.List; -import java.util.stream.Collectors; @RestController @RequestMapping("/api") @@ -73,7 +64,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, + ObjectIdExistsException { EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } @@ -171,4 +163,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..fd48f68e6 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 @@ -10,7 +10,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.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; @@ -22,23 +22,23 @@ public ResponseEntity handleConcurrentModificationException(ConcurrentModific 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()))); - - } - @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({ ObjectIdExistsException.class }) + public ResponseEntity handleObjectIdExistsException(ObjectIdExistsException 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()))); + + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java new file mode 100644 index 000000000..2df1132ac --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/AttributeBundle.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import lombok.Data; + +import javax.persistence.Column; +import javax.persistence.ElementCollection; +import javax.persistence.Entity; +import javax.persistence.Id; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +@Entity(name = "attribute_bundle_definition") +@Data +public class AttributeBundle { + @Column(nullable = false) + @ElementCollection + Set attributes = new HashSet<>(); + + @Column(name = "name", nullable = true) + String name; + + @Id + @Column(name = "resource_id", nullable = false) + String resourceId = UUID.randomUUID().toString(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java new file mode 100644 index 000000000..84a80fce4 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/BundleableAttributeType.java @@ -0,0 +1,40 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import edu.internet2.tier.shibboleth.admin.util.BundleableAttributeTypeValueSerializer; + +@JsonSerialize(using = BundleableAttributeTypeValueSerializer.class) +public enum BundleableAttributeType { + EDUPERSONPRINCIPALNAME("eduPersonPrincipalName"), + UID("uid"), + MAIL("mail"), + SURNAME("surname"), + GIVENNAME("givenName"), + EDUPERSONAFFILIATE("eduPersonAffiliation"), + EDUPERSONSCOPEDAFFILIATION("eduPersonScopedAffiliation"), + EDUPERSONPRIMARYAFFILIATION("eduPersonPrimaryAffiliation"), + EDUPERSONENTITLEMENT("eduPersonEntitlement"), + EDUPERSONASSURANCE("eduPersonAssurance"), + EDUPERSONUNIQUEID("eduPersonUniqueId"), + EMPLOYEENUMBER("employeeNumber"); + + String label; + + BundleableAttributeType(String val) { + label = val; + } + + public String label() {return label;} + + @JsonCreator + public static BundleableAttributeType valueOfLabel(String label) { + for (BundleableAttributeType e : values()) { + if (e.label.equals(label)) { + return e; + } + } + return null; + } + +} \ No newline at end of file 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 deleted file mode 100644 index 990eab2c3..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/EntityIdExistsException.java +++ /dev/null @@ -1,8 +0,0 @@ -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/ObjectIdExistsException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ObjectIdExistsException.java new file mode 100644 index 000000000..f2604acfe --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/ObjectIdExistsException.java @@ -0,0 +1,8 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class ObjectIdExistsException extends Exception { + public ObjectIdExistsException(String entityId) { + super(entityId); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java new file mode 100644 index 000000000..0c9bc2aed --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepository.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +/** + * Repository to manage {@link edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle} instances. + */ +public interface AttributeBundleRepository extends JpaRepository { + List findAll(); + + Optional findByResourceId(String resourceId); + + AttributeBundle save(AttributeBundle target); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java new file mode 100644 index 000000000..f246c7d1b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/AttributeBundleService.java @@ -0,0 +1,46 @@ +package edu.internet2.tier.shibboleth.admin.ui.service; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class AttributeBundleService { + @Autowired + AttributeBundleRepository attributeBundleRepository; + + public AttributeBundle create(AttributeBundle bundle) throws ObjectIdExistsException { + if (attributeBundleRepository.findByResourceId(bundle.getResourceId()).isPresent()) { + throw new ObjectIdExistsException(bundle.getResourceId()); + } + return attributeBundleRepository.save(bundle); + } + + public List findAll() { + return attributeBundleRepository.findAll(); + } + + public void deleteDefinition(String resourceId) throws EntityNotFoundException { + if (attributeBundleRepository.findByResourceId(resourceId).isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find attribute bundle with resource id: [%s] for deletion", resourceId)); + } + attributeBundleRepository.deleteById(resourceId); + } + + public AttributeBundle updateBundle(AttributeBundle bundle) throws EntityNotFoundException { + Optional dbBundle = attributeBundleRepository.findByResourceId(bundle.getResourceId()); + if (dbBundle.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find attribute bundle with resource id: [%s] for update", bundle.getResourceId())); + } + AttributeBundle bundleToUpdate = dbBundle.get(); + bundleToUpdate.setName(bundle.getName()); + bundleToUpdate.setAttributes(bundle.getAttributes()); + return attributeBundleRepository.save(bundleToUpdate); + } +} \ 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 c8c39bbb3..50f29951c 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 @@ -3,7 +3,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; 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.exception.EntityIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; @@ -29,17 +29,18 @@ public interface EntityDescriptorService { * @param ed - JPA EntityDescriptor to base creation on * @return EntityDescriptorRepresentation of the created object * @throws ForbiddenException If user is unauthorized to perform this operation - * @throws EntityIdExistsException If any EntityDescriptor already exists with the same EntityId + * @throws ObjectIdExistsException If any EntityDescriptor already exists with the same EntityId */ - EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException; /** * @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 ObjectIdExistsException If the entity already exists */ - EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, + ObjectIdExistsException; /** * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor 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 d23b16365..1b660ab9f 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,7 +2,7 @@ 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.ObjectIdExistsException; 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; @@ -66,18 +66,19 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, ObjectIdExistsException { return createNew(createRepresentationFromDescriptor(ed)); } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, + ObjectIdExistsException { 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()); + throw new ObjectIdExistsException(edRep.getEntityId()); } EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java new file mode 100644 index 000000000..55aa1ab44 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/BundleableAttributeTypeValueSerializer.java @@ -0,0 +1,28 @@ +package edu.internet2.tier.shibboleth.admin.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import edu.internet2.tier.shibboleth.admin.ui.domain.BundleableAttributeType; + +import java.io.IOException; + +/** + * This simplifies translation to the front end. We use the ENUM on the backend, but the BundleableAttributeType + * is tagged to serialize using this helper. + * Note: The deserialize is done by the setup of the ENUM itself + */ +public class BundleableAttributeTypeValueSerializer extends StdSerializer { + public BundleableAttributeTypeValueSerializer() { + this(null); + } + + public BundleableAttributeTypeValueSerializer(Class t) { + super(t); + } + + @Override + public void serialize(BundleableAttributeType value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeString(value.label()); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5b7b801f1..0556e5b45 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -24,6 +24,7 @@ spring.h2.console.settings.web-allow-others=true # spring.jackson.default-property-inclusion=non_absent spring.jackson.default-property-inclusion=NON_NULL +spring.jackson.mapper.accept-case-insensitive-enums=true # Database Configuration PostgreSQL #spring.datasource.url=jdbc:postgresql://localhost:5432/shibui diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy new file mode 100644 index 000000000..00e624b7e --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/AttributeBundleControllerTests.groovy @@ -0,0 +1,225 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException +import edu.internet2.tier.shibboleth.admin.ui.exception.ObjectIdExistsException +import edu.internet2.tier.shibboleth.admin.ui.repository.AttributeBundleRepository +import edu.internet2.tier.shibboleth.admin.ui.service.AttributeBundleService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.util.NestedServletException +import spock.lang.Specification + +import static org.hamcrest.Matchers.containsInAnyOrder +import static org.springframework.http.MediaType.APPLICATION_JSON +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +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 + +@DataJpaTest(properties = ["spring.jackson.mapper.accept-case-insensitive-enums=true"]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ContextConfiguration(classes = [ShibUIConfiguration, ABCTConfig]) +class AttributeBundleControllerTests extends Specification { + @Autowired + AttributeBundleController controller + + @Autowired + AttributeBundleRepository attributeBundleRepository + + ObjectMapper objectMapper = new ObjectMapper() + + def MockMvc + + @Transactional + def setup() { + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + attributeBundleRepository.deleteAll() + } + + def "GET checks" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: "fetch for no bundles" + def result = mockMvc.perform(get('/api/custom/entity/bundles')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(content().json('[]')) + + when: "add a bundle" + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.saveAndFlush(bundle) + result = mockMvc.perform(get('/api/custom/entity/bundles')) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.[0].name").value("bundleName")) + .andExpect(jsonPath("\$.[0].resourceId").value("randomIDVal")) + .andExpect(jsonPath("\$.[0].attributes", containsInAnyOrder("eduPersonPrincipalName", "surname", "givenName"))) + } + + def "CREATE checks" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: "add a bundle" + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.saveAndFlush(bundle) + + then: "bundle already exists" + try { + mockMvc.perform(post('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(json)) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof ObjectIdExistsException + } + + when: "new bundle" + json = """ + { + "name": "bundle2", + "resourceId": "differentResourceId", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + def result = mockMvc.perform(post('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(json)) + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("bundle2")) + .andExpect(jsonPath("\$.resourceId").value("differentResourceId")) + .andExpect(jsonPath("\$.attributes", containsInAnyOrder("eduPersonPrincipalName", "surname", "givenName"))) + } + + def "test delete" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.save(bundle) + + then: + attributeBundleRepository.findAll().size() == 1 + + // Delete something doesn't exist + try { + mockMvc.perform(delete("/api/custom/entity/bundles/randomIDValdoesntexist")) + false + } catch (NestedServletException expected) { + expected instanceof EntityNotFoundException + } + + when: "Delete what does exist" + def result = mockMvc.perform(delete("/api/custom/entity/bundles/randomIDVal")) + + then: + result.andExpect(status().isNoContent()) + attributeBundleRepository.findAll().isEmpty() + } + + def "Update checks" () { + expect: + attributeBundleRepository.findAll().isEmpty() + + when: "add a bundle" + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + attributeBundleRepository.saveAndFlush(bundle) + + then: "bundle doesn't exist" + bundle.setResourceId("foo") + try { + mockMvc.perform(put('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(objectMapper.writeValueAsString(bundle))) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof EntityNotFoundException + } + + when: "update bundle" + json = """ + { + "name": "bundle2", + "resourceId": "randomIDVal", + "attributes": ["eduPersonUniqueId", "employeeNumber", "givenName"] + } + """ + + def result = mockMvc.perform(put('/api/custom/entity/bundles').contentType(APPLICATION_JSON).content(json)) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.name").value("bundle2")) + .andExpect(jsonPath("\$.resourceId").value("randomIDVal")) + .andExpect(jsonPath("\$.attributes", containsInAnyOrder("eduPersonUniqueId", "employeeNumber", "givenName"))) + } + + // can go away with merge to develop and this extends the base test class + @TestConfiguration + private static class ABCTConfig { + @Bean + AttributeBundleController attributeBundleController(AttributeBundleService attributeBundleService) { + new AttributeBundleController().with { + it.attributeBundleService = attributeBundleService + it + } + } + + @Bean + AttributeBundleService attributeBundleService(AttributeBundleRepository repo) { + new AttributeBundleService().with { + it.attributeBundleRepository = repo + it + } + } + + } +} \ 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 f7b44786a..74ca259a0 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,7 +6,7 @@ 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.ObjectIdExistsException 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 @@ -372,7 +372,7 @@ class EntityDescriptorControllerTests extends Specification { mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) } catch (Exception e) { - e instanceof EntityIdExistsException + e instanceof ObjectIdExistsException } } @@ -618,7 +618,7 @@ class EntityDescriptorControllerTests extends Specification { mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) } catch (Exception e) { - e instanceof EntityIdExistsException + e instanceof ObjectIdExistsException } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy new file mode 100644 index 000000000..0db6a9555 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/AttributeBundleRepositoryTests.groovy @@ -0,0 +1,42 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.AttributeBundle +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@DataJpaTest +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ContextConfiguration(classes = [ShibUIConfiguration]) +class AttributeBundleRepositoryTests extends Specification { + @Autowired + AttributeBundleRepository attributeBundleRepository + + ObjectMapper objectMapper = new ObjectMapper() + + def "test create and fetch" () { + given: + def json = """ + { + "name": "bundleName", + "resourceId": "randomIDVal", + "attributes": ["eduPersonPrincipalName", "surname", "givenName"] + } + """ + + AttributeBundle bundle = objectMapper.readValue(json, AttributeBundle.class) + + when: + def result = attributeBundleRepository.save(bundle) + + then: + result == bundle + } +} \ No newline at end of file