Skip to content

Commit

Permalink
Merge branch 'feature/shibui-2394' of bitbucket.org:unicon/shib-idp-u…
Browse files Browse the repository at this point in the history
…i into feature/shibui-2394
  • Loading branch information
rmathis committed Oct 13, 2022
2 parents abe47db + a91b683 commit bd80aed
Show file tree
Hide file tree
Showing 17 changed files with 241 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -49,15 +52,16 @@ class DevConfig {
RoleRepository roleRepository,
EntityDescriptorRepository entityDescriptorRepository,
OpenSamlObjects openSamlObjects,
IGroupService groupService) {
IGroupService groupService,
ApproversRepository approversRepository) {

this.userRepository = adminUserRepository
this.metadataResolverRepository = metadataResolverRepository
this.roleRepository = roleRepository
this.entityDescriptorRepository = entityDescriptorRepository
this.openSamlObjects = openSamlObjects
this.groupsRepository = groupsRepository

this.approversRepository = approversRepository
groupService.ensureAdminGroupExists()
}

Expand Down Expand Up @@ -85,7 +89,29 @@ class DevConfig {
}
}
groupsRepository.flush()


List<Group> 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<Approvers> 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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> approved = new ArrayList<>();
private List<String> approvedBy = new ArrayList<>();

@OneToOne(cascade = CascadeType.ALL)
@NotAudited
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,4 +38,10 @@ public interface EntityDescriptorRepository extends JpaRepository<EntityDescript
*/
@Deprecated
List<EntityDescriptor> findAllByIdOfOwnerIsNull();

@Query(value = "select e from EntityDescriptor e" +
" where e.idOfOwner in (:groupIds)" +
" and e.serviceEnabled = false" +
" and e.approved = false")
List<EntityDescriptorProjection> getEntityDescriptorsNeedingApproval(@Param("groupIds") List<String> groupIds);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.internet2.tier.shibboleth.admin.ui.security.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NoArgsConstructor;

Expand All @@ -8,6 +9,7 @@
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.OneToMany;
import javax.persistence.Transient;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
Expand All @@ -20,8 +22,24 @@
public class Approvers {
@Id
@Column(name = "resource_id")
@JsonIgnore
private String resourceId = UUID.randomUUID().toString();

@ManyToMany
@JsonIgnore
private List<Group> approverGroups = new ArrayList<>();

@Transient
private List<String> approverGroupIds = new ArrayList<>();

public List<String> getApproverGroupIds() {
if (approverGroupIds.isEmpty()) {
approverGroups.forEach(group -> approverGroupIds.add(group.getResourceId()));
}
return approverGroupIds;
}

public void setApproverGroups(List<Group> appGroups) {
this.approverGroups = appGroups;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class Group implements Owner {
public static Group ADMIN_GROUP;

@Transient
@JsonIgnore
List<String> approveForList = new ArrayList<>();

@Column(name = "group_description")
Expand Down Expand Up @@ -107,4 +108,9 @@ public List<String> getApproveForList() {
}
return approveForList;
}

@Override
public String toString() {
return "Group resourceId=" + resourceId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,13 @@ public List<Group> findAll() {
return groupRepository.findAll();
}

@Override
public Group updateGroup(Group group) throws PersistentEntityNotFound, InvalidGroupRegexException {
manageApproversList(group); // have to make sure that approvers have been saved before a fetch or we can get data integrity errors on lookup...
Group g = find(group.getResourceId());
if (g == null) {
throw new PersistentEntityNotFound(String.format("Unable to find group with resource id: [%s] and name: [%s]",
group.getResourceId(), group.getName()));
private List<Group> getGroupListFromIds(List<String> approverGroupIds) {
List<Group> result = new ArrayList<>();
for (String id : approverGroupIds) {
Group g = find(id);
result.add(g);
}
validateGroupRegex(group);
return groupRepository.save(group);
return result;
}

private void manageApproversList(Group group) {
Expand All @@ -134,14 +131,26 @@ private void manageApproversList(Group group) {
List<Approvers> updatedApprovers = new ArrayList<>();
group.getApproversList().forEach(approvers -> {
Approvers savedApprovers = approversRepository.findByResourceId(approvers.getResourceId());
savedApprovers = savedApprovers == null ? approvers : savedApprovers;
savedApprovers.setApproverGroups(approvers.getApproverGroups());
savedApprovers = savedApprovers == null ? approversRepository.save(approvers) : savedApprovers;
savedApprovers.setApproverGroups(getGroupListFromIds(approvers.getApproverGroupIds()));
Approvers updatedApp = approversRepository.save(savedApprovers);
updatedApprovers.add(updatedApp);
});
group.setApproversList(updatedApprovers);
}

@Override
public Group updateGroup(Group group) throws PersistentEntityNotFound, InvalidGroupRegexException {
manageApproversList(group); // have to make sure that approvers have been saved before a fetch or we can get data integrity errors on lookup...
Group g = find(group.getResourceId());
if (g == null) {
throw new PersistentEntityNotFound(String.format("Unable to find group with resource id: [%s] and name: [%s]",
group.getResourceId(), group.getName()));
}
validateGroupRegex(group);
return groupRepository.save(group);
}

/**
* If the regex is blank simply return
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,6 @@ EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String resour
EntityDescriptorRepresentation updateGroupForEntityDescriptor(String resourceId, String groupId);

EntityDescriptorRepresentation changeApproveStatusOfEntityDescriptor(String resourceId, boolean status) throws PersistentEntityNotFound, ForbiddenException;

List<EntityDescriptorProjection> getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -434,6 +438,16 @@ public List<EntityDescriptorProjection> getAllEntityDescriptorProjectionsBasedOn
}
}

/**
* Based on the current users group, find those entities that the user can approve that need approval
*/
@Override
public List<EntityDescriptorProjection> getAllEntityDescriptorProjectionsNeedingApprovalBasedOnUserAccess() {
List<String> groupsToApprove = userService.getGroupsCurrentUserCanApprove();
List<EntityDescriptorProjection> result = entityDescriptorRepository.getEntityDescriptorsNeedingApproval(groupsToApprove);
return result;
}

@Override
public List<String> getAttributeReleaseListFromAttributeList(List<Attribute> attributeList) {
if (attributeList == null) {
Expand Down Expand Up @@ -512,10 +526,13 @@ public EntityDescriptorRepresentation updateEntityDescriptorEnabledStatus(String
// check to see if approvals have been completed
int approvedCount = ed.approvedCount();
List<Approvers> 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);
}
Expand Down
1 change: 1 addition & 0 deletions backend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ 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
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

#Envers versioning
spring.jpa.properties.org.hibernate.envers.store_data_at_delete=true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"))

Expand All @@ -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"))

Expand All @@ -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"))
Expand All @@ -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
}
}
Loading

0 comments on commit bd80aed

Please sign in to comment.