diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy index a644a58a0..70892cbb7 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy @@ -12,9 +12,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadat 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.repository.MetadataResolverRepository +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.security.repository.ApproversRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository @@ -39,6 +41,7 @@ class DevConfig { private final OpenSamlObjects openSamlObjects private final RoleRepository roleRepository private final UserRepository userRepository + private final ApproversRepository approversRepository @Autowired private UserService userService @@ -49,7 +52,8 @@ class DevConfig { RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository, OpenSamlObjects openSamlObjects, - IGroupService groupService) { + IGroupService groupService, + ApproversRepository approversRepository) { this.userRepository = adminUserRepository this.metadataResolverRepository = metadataResolverRepository @@ -57,7 +61,7 @@ class DevConfig { this.entityDescriptorRepository = entityDescriptorRepository this.openSamlObjects = openSamlObjects this.groupsRepository = groupsRepository - + this.approversRepository = approversRepository groupService.ensureAdminGroupExists() } @@ -85,7 +89,29 @@ class DevConfig { } } groupsRepository.flush() - + + List apprGroups = new ArrayList<>() + String[] groupNames = ['XXX', 'YYY', 'ZZZ'] + groupNames.each {name -> { + Group group = new Group().with({ + it.name = name + it.description = name + it.resourceId = name + it + }) + if (name != "ZZZ") { + apprGroups.add(groupsRepository.save(group)) + } else { + Approvers approvers = new Approvers() + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupsRepository.save(group) + } + }} + groupsRepository.flush() + if (roleRepository.count() == 0) { def roles = [new Role().with { name = 'ROLE_ADMIN' 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 0a6cda0b3..3e1f4db27 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 @@ -85,6 +85,12 @@ public ResponseEntity getAll() throws ForbiddenException { return ResponseEntity.ok(entityDescriptorService.getAllEntityDescriptorProjectionsBasedOnUserAccess()); } + @GetMapping("/EntityDescriptors/needsApproval") + @Transactional + public ResponseEntity getAllNeedingApproval() throws ForbiddenException { + return ResponseEntity.ok(entityDescriptorService.getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess()); + } + @GetMapping("/EntityDescriptor/{resourceId}/Versions") @Transactional public ResponseEntity getAllVersions(@PathVariable String resourceId) throws PersistentEntityNotFound, ForbiddenException { 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 33e7ce6d1..872f78b1d 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 @@ -47,13 +47,17 @@ public class EntityDescriptor extends AbstractDescriptor implements org.opensaml @NotAudited private AffiliationDescriptor affiliationDescriptor; + @Getter + @Setter + private boolean approved; + @OneToOne(cascade = CascadeType.ALL) @NotAudited private AttributeAuthorityDescriptor attributeAuthorityDescriptor; @ElementCollection (fetch = FetchType.EAGER) @EqualsAndHashCode.Exclude - private List approved = new ArrayList<>(); + private List approvedBy = new ArrayList<>(); @OneToOne(cascade = CascadeType.ALL) @NotAudited @@ -317,16 +321,16 @@ public OwnableType getOwnableType() { } public void addApproval(Group group) { - approved.add(group.getName()); + approvedBy.add(group.getName()); } public int approvedCount() { - return approved.size(); + return approvedBy.size(); } public void removeLastApproval() { - if (!approved.isEmpty()) { - approved.remove(approved.size() - 1); + if (!approvedBy.isEmpty()) { + approvedBy.remove(approvedBy.size() - 1); } } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java index bb2b275d6..a4ff5c43f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -3,6 +3,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.stream.Stream; @@ -37,4 +38,10 @@ public interface EntityDescriptorRepository extends JpaRepository findAllByIdOfOwnerIsNull(); + + @Query(value = "select e from EntityDescriptor e" + + " where e.idOfOwner in (:groupIds)" + + " and e.serviceEnabled = false" + + " and e.approved = false") + List getEntityDescriptorsNeedingApproval(@Param("groupIds") List groupIds); } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java index 14597deb2..cc2570320 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/Group.java @@ -34,6 +34,7 @@ public class Group implements Owner { public static Group ADMIN_GROUP; @Transient + @JsonIgnore List approveForList = new ArrayList<>(); @Column(name = "group_description") 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 1f37b83bc..8ee4adb52 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 @@ -124,4 +124,6 @@ EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resour EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId); EntityDescriptorRepresentation changeApproveStatusOfEntityDescriptor(String resourceId, boolean status) throws PersistentEntityNotFound, ForbiddenException; + + List getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess(); } \ 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 0b1f67932..667477f09 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 @@ -91,10 +91,14 @@ public EntityDescriptorRepresentation changeApproveStatusOfEntityDescriptor(Stri throw new ForbiddenException("You do not have the permissions necessary to approve this entity descriptor."); } ed.addApproval(userService.getCurrentUserGroup()); + Group ownerGroup = groupService.find(ed.getIdOfOwner()); + ed.setApproved(ed.approvedCount() == ownerGroup.getApproversList().size()); // safe check in case of weird race conditions from the UI ed = entityDescriptorRepository.save(ed); } } else { // un-approve ed.removeLastApproval(); + Group ownerGroup = groupService.find(ed.getIdOfOwner()); + ed.setApproved(ed.approvedCount() == ownerGroup.getApproversList().size()); // safe check in case of weird race conditions from the UI ed = entityDescriptorRepository.save(ed); } return createRepresentationFromDescriptor(ed); @@ -434,6 +438,16 @@ public List getAllEntityDescriptorProjectionsBasedOn } } + /** + * Based on the current users group, find those entities that the user can approve that need approval + */ + @Override + public List getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess() { + List groupsToApprove = userService.getGroupsCurrentUserCanApprove(); + List result = entityDescriptorRepository.getEntityDescriptorsNeedingApproval(groupsToApprove); + return result; + } + @Override public List getAttributeReleaseListFromAttributeList(List attributeList) { if (attributeList == null) { @@ -512,10 +526,13 @@ public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String // 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) { + if (status == true && !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); + if (status == true) { + ed.setApproved(true); + } ed = entityDescriptorRepository.save(ed); return createRepresentationFromDescriptor(ed); } 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 index b18c40aad..2a5b46883 100644 --- 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 @@ -110,27 +110,34 @@ class ApproveControllerTests extends AbstractBaseDataJpaTest { @WithMockUser(value = "AUser", roles = ["USER"]) def 'Owner group cannot approve their own entity descriptor'() { expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false try { mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) } catch (Exception e) { e instanceof ForbiddenException } + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false } @WithMockUser(value = "DUser", roles = ["USER"]) def 'non-approver group cannot approve entity descriptor'() { expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false try { mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) } catch (Exception e) { e instanceof ForbiddenException } + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false } @WithMockUser(value = "BUser", roles = ["USER"]) def 'Approver group can approve an entity descriptor'() { + expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + when: def result = mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) @@ -139,10 +146,14 @@ class ApproveControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.approved").value(true)) + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() } @WithMockUser(value = "BUser", roles = ["USER"]) def 'Approver can approve and un-approve an entity descriptor'() { + expect: + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false + when: def result = mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/approve")) @@ -151,6 +162,7 @@ class ApproveControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.approved").value(true)) + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() when: def result2 = mockMvc.perform(patch("/api/approve/entityDescriptor/" + defaultEntityDescriptorResourceId + "/unapprove")) @@ -160,6 +172,6 @@ class ApproveControllerTests extends AbstractBaseDataJpaTest { .andExpect(jsonPath("\$.id").value(defaultEntityDescriptorResourceId)) .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.approved").value(false)) - + entityDescriptorRepository.findByResourceId(defaultEntityDescriptorResourceId).isApproved() == false } } \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy index d40e2bd1c..95c8dc5e6 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/springsecurity/AdminUserServiceTests.groovy @@ -5,6 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.DevConfig 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.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.ApproversRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository @@ -74,9 +75,10 @@ class AdminUserServiceTests extends AbstractBaseDataJpaTest { @Bean DevConfig devConfig(UserRepository adminUserRepository, GroupsRepository groupsRepository, IGroupService groupService, MetadataResolverRepository metadataResolverRepository, OpenSamlObjects openSamlObjects, UserService userService, - RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository) { + RoleRepository roleRepository, EntityDescriptorRepository entityDescriptorRepository, + ApproversRepository approversRepository) { DevConfig dc = new DevConfig( adminUserRepository, groupsRepository, metadataResolverRepository, roleRepository, - entityDescriptorRepository, openSamlObjects, groupService).with { + entityDescriptorRepository, openSamlObjects, groupService, approversRepository).with { it.userService = userService it } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index 9e65bf28a..6e7e2cf43 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -15,6 +15,12 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSso import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup import edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator 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.Role +import edu.internet2.tier.shibboleth.admin.ui.security.model.User import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils @@ -24,6 +30,8 @@ import org.springframework.boot.test.json.JacksonTester import org.springframework.context.annotation.PropertySource import org.springframework.core.io.DefaultResourceLoader import org.springframework.mock.http.MockHttpInputMessage +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.transaction.annotation.Transactional import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import org.xmlunit.diff.DefaultNodeMatcher @@ -52,14 +60,64 @@ class JPAEntityDescriptorServiceImplTests extends AbstractBaseDataJpaTest { RandomGenerator generator JacksonTester jacksonTester + @Autowired + EntityDescriptorRepository entityDescriptorRepository + + @Transactional def setup() { JacksonTester.initFields(this, mapper) generator = new RandomGenerator() EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects EntityDescriptorConversionUtils.entityService = entityService openSamlObjects.init() + + groupService.clearAllForTesting() + List apprGroups = new ArrayList<>() + String[] groupNames = ['BBB', 'CCC', 'EEE', '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 + }) + Approvers approvers = new Approvers() + apprGroups = new ArrayList<>() + apprGroups.add(groupRepository.findByResourceId('BBB')) + approvers.setApproverGroups(apprGroups) + List apprList = new ArrayList<>() + apprList.add(approversRepository.save(approvers)) + group.setApproversList(apprList) + groupRepository.save(group) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "bbUser", roles:[userRole.get()], password: "foo") + user.setGroup(groupRepository.findByResourceId("BBB")) + userService.save(user) + + entityManager.flush() + entityManager.clear() } + + def "simple Entity Descriptor"() { when: def expected = '''