From 1af89c5a26d36237c2b3548c36d05415ac7a8ae4 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 4 Oct 2022 09:07:06 -0700 Subject: [PATCH] SHIBUI-2394 Incremental commit - non working tests --- .../ui/controller/ActivateController.java | 6 +- .../ui/controller/ApprovalController.java | 31 ++++ ...> ApproveAndActivateExceptionHandler.java} | 4 +- .../EntityDescriptorController.java | 3 +- .../admin/ui/domain/EntityDescriptor.java | 20 ++- .../ui/security/service/UserService.java | 8 + .../ui/service/EntityDescriptorService.java | 2 + .../JPAEntityDescriptorServiceImpl.java | 44 ++++- .../src/main/resources/application.properties | 1 + .../controller/ActivateControllerTests.groovy | 157 ++++++++++++++++++ .../controller/ApproveControllerTests.groovy | 144 ++++++++++++++++ 11 files changed, 403 insertions(+), 17 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java rename backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/{ActivateExceptionHandler.java => ApproveAndActivateExceptionHandler.java} (93%) create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java index e43b88e1e..14e5894f5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateController.java @@ -47,8 +47,7 @@ public ResponseEntity enableEntityDescriptor(@PathVariable String resourceId, @PatchMapping(path = "/MetadataResolvers/{metadataResolverId}/Filter/{resourceId}/{mode}") @Transactional - public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws - PersistentEntityNotFound, ForbiddenException, ScriptException { + public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException, ScriptException { boolean status = "enable".equalsIgnoreCase(mode); MetadataFilter persistedFilter = filterService.updateFilterEnabledStatus(metadataResolverId, resourceId, status); return ResponseEntity.ok(persistedFilter); @@ -56,8 +55,7 @@ public ResponseEntity enableFilter(@PathVariable String metadataResolverId, @ @PatchMapping("/MetadataResolvers/{resourceId}/{mode}") @Transactional - public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws - PersistentEntityNotFound, ForbiddenException, MetadataFileNotFoundException, InitializationException { + public ResponseEntity enableProvider(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException, MetadataFileNotFoundException, InitializationException { boolean status = "enable".equalsIgnoreCase(mode); MetadataResolver existingResolver = metadataResolverService.findByResourceId(resourceId); existingResolver.setEnabled(status); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java new file mode 100644 index 000000000..f3945ef47 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApprovalController.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.PersistentEntityNotFound; +import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.tags.Tags; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/approve") +@Tags(value = {@Tag(name = "approve")}) +public class ApprovalController { + @Autowired + private EntityDescriptorService entityDescriptorService; + + @PatchMapping(path = "/entityDescriptor/{resourceId}/{mode}") + @Transactional + public ResponseEntity approveEntityDescriptor(@PathVariable String resourceId, @PathVariable String mode) throws PersistentEntityNotFound, ForbiddenException { +// boolean status = "approve".equalsIgnoreCase(mode); // can we un-approve? + EntityDescriptorRepresentation edr = entityDescriptorService.approveEntityDescriptor(resourceId); + return ResponseEntity.ok(edr); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveAndActivateExceptionHandler.java similarity index 93% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveAndActivateExceptionHandler.java index fe6f7c0f2..9f58ce8e1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveAndActivateExceptionHandler.java @@ -16,8 +16,8 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; import edu.internet2.tier.shibboleth.admin.ui.exception.InitializationException; -@ControllerAdvice(assignableTypes = {ActivateController.class}) -public class ActivateExceptionHandler extends ResponseEntityExceptionHandler { +@ControllerAdvice(assignableTypes = {ActivateController.class, ApprovalController.class}) +public class ApproveAndActivateExceptionHandler extends ResponseEntityExceptionHandler { @ExceptionHandler({ PersistentEntityNotFound.class }) public ResponseEntity handleEntityNotFoundException(PersistentEntityNotFound e, WebRequest request) { 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 e57870cb9..0a6cda0b3 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 @@ -66,8 +66,7 @@ public EntityDescriptorController(EntityDescriptorVersionService versionService) @PostMapping("/EntityDescriptor") @Transactional - public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) - throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java index 185b43918..d1885ae14 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/EntityDescriptor.java @@ -1,26 +1,24 @@ package edu.internet2.tier.shibboleth.admin.ui.domain; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.google.common.base.MoreObjects; import com.google.common.collect.Lists; - +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownable; import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnableType; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; - import org.hibernate.envers.Audited; import org.hibernate.envers.NotAudited; -import org.hibernate.envers.RelationTargetAuditMode; import org.opensaml.core.xml.XMLObject; import org.springframework.util.StringUtils; import javax.annotation.Nullable; import javax.persistence.CascadeType; +import javax.persistence.ElementCollection; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.OrderColumn; @@ -53,6 +51,10 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; + @ElementCollection (fetch = FetchType.EAGER) + @EqualsAndHashCode.Exclude + private List approved = new ArrayList<>(); + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AuthnAuthorityDescriptor authnAuthorityDescriptor; @@ -313,4 +315,12 @@ public OwnableType getOwnableType() { @Override public ActivatableType getActivatableType() { return ENTITY_DESCRIPTOR; } + + public void addApproval(Group group) { + approved.add(group.getName()); + } + + public int approvedCount() { + return approved.size(); + } } \ No newline at end of file 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 dfe21708a..25f152a90 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 @@ -53,6 +53,14 @@ public UserService(IGroupService groupService, OwnershipRepository ownershipRepo this.userRepository = userRepository; } + public boolean currentUserCanApprove(List approverGroups) { + if (currentUserIsAdmin()) { + return true; + } + Group currentUserGroup = getCurrentUserGroup(); + return approverGroups.contains(currentUserGroup); + } + public boolean currentUserCanEnable(IActivatable activatableObject) { if (currentUserIsAdmin()) { return true; } switch (activatableObject.getActivatableType()) { 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 fa8fc62f8..e5931f93a 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 @@ -122,4 +122,6 @@ EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resour boolean entityExists(String entityID); EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId); + + EntityDescriptorRepresentation approveEntityDescriptor(String resourceId) throws PersistentEntityNotFound, ForbiddenException; } \ 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 2fc27dfaa..21607029b 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 @@ -23,6 +23,7 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorProjection; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.model.Owner; import edu.internet2.tier.shibboleth.admin.ui.security.model.OwnerType; @@ -74,6 +75,25 @@ public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private UserService userService; + @Override + public EntityDescriptorRepresentation approveEntityDescriptor(String resourceId) throws PersistentEntityNotFound, ForbiddenException { + EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); + if (ed == null) { + throw new PersistentEntityNotFound("Entity with resourceid[" + resourceId + "] was not found for approval"); + } + int approvedCount = ed.approvedCount(); + List approversList = groupService.find(ed.getIdOfOwner()).getApproversList(); + if (!approversList.isEmpty() && approversList.size() > approvedCount) { + Approvers approvers = approversList.get(approvedCount); // yea for index zero - use the count to get the next approvers + if (!userService.currentUserCanApprove(approvers.getApproverGroups())) { + throw new ForbiddenException("You do not have the permissions necessary to approve this entity descriptor."); + } + ed.addApproval(userService.getCurrentUserGroup()); + ed = entityDescriptorRepository.save(ed); + } + return createRepresentationFromDescriptor(ed); + } + private EntityDescriptor buildDescriptorFromRepresentation(final EntityDescriptor ed, final EntityDescriptorRepresentation representation) { ed.setEntityID(representation.getEntityId()); ed.setIdOfOwner(representation.getIdOfOwner()); @@ -128,8 +148,7 @@ public EntityDescriptorRepresentation updateGroupForEntityDescriptor(String reso } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) - throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, ObjectIdExistsException, InvalidPatternMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } @@ -167,6 +186,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope representation.setCreatedBy(ed.getCreatedBy()); representation.setCurrent(ed.isCurrent()); representation.setIdOfOwner(ed.getIdOfOwner()); + representation.setApproved(isEntityDescriptorApproved(ed)); if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getSupportedProtocols().size() > 0) { ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptorRepresentation = representation.getServiceProviderSsoDescriptor(true); @@ -362,6 +382,17 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope return representation; } + private boolean isEntityDescriptorApproved(EntityDescriptor ed) { + if (ed.isServiceEnabled()) { + return true; + } + Group ownerGroup = groupService.find(ed.getIdOfOwner()); + if (ownerGroup == null) { + ownerGroup = Group.ADMIN_GROUP; // This should only happen in the large number of tests that were written prior to group implementation + } + return ed.approvedCount() >= ownerGroup.getApproversList().size(); + } + @Override public void delete(String resourceId) throws ForbiddenException, PersistentEntityNotFound { EntityDescriptor ed = getEntityDescriptorByResourceId(resourceId); @@ -424,8 +455,7 @@ public Map getRelyingPartyOverridesRepresentationFromAttributeLi } @Override - public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) - throws ForbiddenException, PersistentEntityNotFound, InvalidPatternMatchException { + public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, PersistentEntityNotFound, InvalidPatternMatchException { EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId()); if (existingEd == null) { throw new PersistentEntityNotFound(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); @@ -473,6 +503,12 @@ public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String if (!userService.currentUserCanEnable(ed)) { throw new ForbiddenException("You do not have the permissions necessary to change the enable status of this entity descriptor."); } + // check to see if approvals have been completed + int approvedCount = ed.approvedCount(); + List approversList = groupService.find(ed.getIdOfOwner()).getApproversList(); + if (!ed.isServiceEnabled() && !userService.currentUserIsAdmin() && approversList.size() > approvedCount) { + throw new ForbiddenException("Approval must be completed before you can change the enable status of this entity descriptor."); + } ed.setServiceEnabled(status); ed = entityDescriptorRepository.save(ed); return createRepresentationFromDescriptor(ed); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 109e7c30f..6593646f9 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -48,6 +48,7 @@ spring.liquibase.change-log=db/changelog/changelog.sql spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl spring.jpa.show-sql=false +spring.jpa.properties.hibernate.show_sql=false spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.check_nullability=true spring.jpa.hibernate.use-new-id-generator-mappings=true diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy new file mode 100644 index 000000000..8dfd0cf29 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ActivateControllerTests.groovy @@ -0,0 +1,157 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers +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.service.EntityDescriptorService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.ui.util.WithMockAdmin +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Subject + +import javax.transaction.Transactional + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +// TODO: This is only checking activation for EntityDescriptors. Expanding for resolvers not included +class ActivateControllerTests extends AbstractBaseDataJpaTest { + @Subject + def controller + + @Autowired + ObjectMapper mapper + + @Autowired + EntityService entityService + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + private EntityDescriptorService entDescriptorService; + + @Autowired + OpenSamlObjects openSamlObjects + + def defaultEntityDescriptorResourceId + def mockMvc + + @Transactional + def setup() { + controller = new ActivateController() + controller.entityDescriptorService = entDescriptorService + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['BBB', 'CCC', 'AAA'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "AAA") { + apprGroups.add(groupRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + } + }} + Group group = new Group().with({ + it.name = 'DDD' + it.description = 'DDD' + it.resourceId = 'DDD' + it + }) + groupRepository.save(group) + entityManager.flush() + entityManager.clear() + + Optional userRole = roleRepository.findByName("ROLE_ENABLE") + User user = new User(username: "AUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("AAA")) + userService.save(user) + user = new User(username: "BUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("BBB")) + userService.save(user) + user = new User(username: "DUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("DDD")) + userService.save(user) + + entityManager.flush() + entityManager.clear() + + EntityDescriptor entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'AAA') + entityDescriptor = entityDescriptorRepository.save(entityDescriptor) + + defaultEntityDescriptorResourceId = entityDescriptor.getResourceId() + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group cannot activate their own entity descriptor without approvals'() { + expect: + try { + mockMvc.perform(patch("/api/activate/entityDescriptor/" + defaultEntityDescriptorResourceId + "/enable")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockUser(value = "DUser", roles = ["USER"]) + def 'non-owner group cannot activate entity descriptor'() { + expect: + try { + mockMvc.perform(patch("/api/activate/entityDescriptor/" + defaultEntityDescriptorResourceId + "/enable")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockAdmin + def 'Admin can activate an entity descriptor without approval'() { + when: + def result = mockMvc.perform(patch("/api/activate/entityDescriptor/" + defaultEntityDescriptorResourceId + "/enable")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group can enable their own entity descriptor with approvals'() { + when: + EntityDescriptor entityDescriptor = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'AAA') + entityDescriptor.addApproval(groupService.find("CCC")) + entityDescriptorRepository.save(entityDescriptor) + + def result = mockMvc.perform(patch("/api/activate/entityDescriptor/uuid-2/enable")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value('uuid-2')) + .andExpect(jsonPath("\$.serviceEnabled").value(true)) + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy new file mode 100644 index 000000000..04ff8ceda --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/ApproveControllerTests.groovy @@ -0,0 +1,144 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.AbstractBaseDataJpaTest +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.security.model.Approvers +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.service.EntityDescriptorService +import edu.internet2.tier.shibboleth.admin.ui.service.EntityService +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.client.RestTemplate +import spock.lang.Subject + +import javax.transaction.Transactional + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +class ApproveControllerTests extends AbstractBaseDataJpaTest { + @Subject + def controller + + @Autowired + ObjectMapper mapper + + @Autowired + EntityService entityService + + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Autowired + private EntityDescriptorService entDescriptorService; + + @Autowired + OpenSamlObjects openSamlObjects + + def defaultEntityDescriptorResourceId + def mockMvc + + @Transactional + def setup() { + controller = new ApprovalController() + controller.entityDescriptorService = entDescriptorService + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) + + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['BBB', 'CCC', 'AAA'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "AAA") { + apprGroups.add(groupRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + } + }} + Group group = new Group().with({ + it.name = 'DDD' + it.description = 'DDD' + it.resourceId = 'DDD' + it + }) + groupRepository.save(group) + entityManager.flush() + entityManager.clear() + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "AUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("AAA")) + userService.save(user) + user = new User(username: "BUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("BBB")) + userService.save(user) + user = new User(username: "DUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("DDD")) + userService.save(user) + + entityManager.flush() + entityManager.clear() + + EntityDescriptor entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: 'AAA') + entityDescriptor = entityDescriptorRepository.save(entityDescriptor) + + defaultEntityDescriptorResourceId = entityDescriptor.getResourceId() + } + + @WithMockUser(value = "AUser", roles = ["USER"]) + def 'Owner group cannot approve their own entity descriptor'() { + expect: + try { + mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockUser(value = "DUser", roles = ["USER"]) + def 'non-approver group cannot approve entity descriptor'() { + expect: + try { + mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) + } + catch (Exception e) { + e instanceof ForbiddenException + } + } + + @WithMockUser(value = "BUser", roles = ["USER"]) + def 'Approver group can approve an entity descriptor'() { + when: + def result = mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) + + then: + result.andExpect(status().isOk()) + .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.approved").value(true)) + } + +} \ No newline at end of file