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..4d0009523 --- /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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java new file mode 100644 index 000000000..12cfe7025 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesController.java @@ -0,0 +1,65 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +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; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IRolesService; + +@RestController +@RequestMapping("/api/admin/roles") +public class RolesController { + @Autowired + private IRolesService rolesService; + + @Secured("ROLE_ADMIN") + @PostMapping + @Transactional + public ResponseEntity create(@RequestBody Role role) throws RoleExistsConflictException { + Role result = rolesService.createRole(role); + return ResponseEntity.status(HttpStatus.CREATED).body(result); + } + + @Secured("ROLE_ADMIN") + @DeleteMapping("/{resourceId}") + @Transactional + public ResponseEntity delete(@PathVariable String resourceId) throws EntityNotFoundException, RoleDeleteException { + rolesService.deleteDefinition(resourceId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + @Transactional(readOnly = true) + public ResponseEntity getAll() { + return ResponseEntity.ok(rolesService.findAll()); + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNotFoundException { + Role role = rolesService.findByResourceId(resourceId); + return ResponseEntity.ok(role); + } + + @Secured("ROLE_ADMIN") + @PutMapping + @Transactional + public ResponseEntity update(@RequestBody Role role) throws EntityNotFoundException { + Role result = rolesService.updateRole(role); + return ResponseEntity.ok(result); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java new file mode 100644 index 000000000..e4b840f1a --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/RolesExceptionHandler.java @@ -0,0 +1,38 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.controller; + +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 org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; + +@ControllerAdvice(assignableTypes = {RolesController.class}) +public class RolesExceptionHandler 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({ RoleDeleteException.class }) + public ResponseEntity handleForbiddenAccess(RoleDeleteException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/role/{resourceId}").build().toUri()); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), e.getMessage())); + + } + + @ExceptionHandler({RoleExistsConflictException.class}) + public ResponseEntity handleRoleExistsConflictException(RoleExistsConflictException 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/security/exception/RoleDeleteException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java new file mode 100644 index 000000000..a7a35ab4f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleDeleteException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleDeleteException extends Exception { + public RoleDeleteException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java new file mode 100644 index 000000000..f5364c6ff --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/RoleExistsConflictException.java @@ -0,0 +1,9 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class RoleExistsConflictException extends Exception { + + public RoleExistsConflictException(String message) { + super(message); + } + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java index 64792774d..63484618d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Role.java @@ -1,6 +1,16 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.ManyToMany; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -8,14 +18,6 @@ import lombok.Setter; import lombok.ToString; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.ManyToMany; -import java.util.HashSet; -import java.util.Set; - /** * Models a basic administrative role concept in the system. * @@ -29,24 +31,27 @@ @ToString(exclude = "users") public class Role extends AbstractAuditable { - public Role(String name) { - this.name = name; - } - - public Role(String name, int rank) { - this.name = name; - this.rank = rank; - } - @Column(unique = true) private String name; @Column(name = "ROLE_RANK") private int rank; + @Column(name = "resource_id") + String resourceId = UUID.randomUUID().toString(); + //Ignore properties annotation here is to prevent stack overflow recursive error during JSON serialization @JsonIgnoreProperties("roles") @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY) private Set users = new HashSet<>(); + public Role(String name) { + this.name = name; + } + + public Role(String name, int rank) { + this.name = name; + this.rank = rank; + } + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java index 120f2938e..fb77d0e9d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/RoleRepository.java @@ -12,5 +12,9 @@ */ public interface RoleRepository extends JpaRepository { + void deleteByResourceId(String resourceId); + Optional findByName(final String name); + + Optional findByResourceId(String resourceId); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java new file mode 100644 index 000000000..26653d241 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IRolesService.java @@ -0,0 +1,22 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; + +public interface IRolesService { + + Role createRole(Role role) throws RoleExistsConflictException; + + Role updateRole(Role role) throws EntityNotFoundException; + + List findAll(); + + Role findByResourceId(String resourceId) throws EntityNotFoundException; + + void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java new file mode 100644 index 000000000..cfbe57fb0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/RolesServiceImpl.java @@ -0,0 +1,62 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleDeleteException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.RoleExistsConflictException; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; + +@Service +public class RolesServiceImpl implements IRolesService { + @Autowired + private RoleRepository roleRepository; + + @Override + public Role createRole(Role role) throws RoleExistsConflictException { + Optional found = roleRepository.findByName(role.getName()); + // If already defined, we don't want to create a new one, nor do we want this call update the definition + if (found.isPresent()) { + throw new RoleExistsConflictException( + String.format("Call update (PUT) to modify the role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } + + @Override + public void deleteDefinition(String resourceId) throws EntityNotFoundException, RoleDeleteException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isPresent() && !found.get().getUsers().isEmpty()) { + throw new RoleDeleteException(String.format("Unable to delete role with resource id: [%s] - remove role from all users first", resourceId)); + } + roleRepository.deleteByResourceId(resourceId); + } + + @Override + public List findAll() { + return roleRepository.findAll(); + } + + @Override + public Role findByResourceId(String resourceId) throws EntityNotFoundException { + Optional found = roleRepository.findByResourceId(resourceId); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with resource id: [%s]", resourceId)); + } + return found.get(); + } + + @Override + public Role updateRole(Role role) throws EntityNotFoundException { + Optional found = roleRepository.findByName(role.getName()); + if (found.isEmpty()) { + throw new EntityNotFoundException(String.format("Unable to find role with name: [%s]", role.getName())); + } + return roleRepository.save(role); + } +}