diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy index b7f31270a..936093615 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/envers/EntityDescriptorEnversVersioningTests.groovy @@ -42,6 +42,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSso 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.service.EntityDescriptorService +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -80,6 +81,10 @@ class EntityDescriptorEnversVersioningTests extends Specification { @Autowired OpenSamlObjects openSamlObjects + + def setup() { + EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects + } def "test versioning with contact persons"() { setup: @@ -303,7 +308,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) //Groovy FTW - able to call any private methods on ANY object. Get first revision - UIInfo uiinfo = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + UIInfo uiinfo = EntityDescriptorConversionUtils.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) then: entityDescriptorHistory.size() == 1 @@ -336,9 +341,9 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) //Get second revision - uiinfo = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + uiinfo = EntityDescriptorConversionUtils.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) //And initial revision - def uiinfoInitialRevision = entityDescriptorService.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + def uiinfoInitialRevision = EntityDescriptorConversionUtils.getUIInfo(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) then: entityDescriptorHistory.size() == 2 @@ -389,7 +394,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { //Get initial revision SPSSODescriptor spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) KeyDescriptor keyDescriptor = spssoDescriptor.keyDescriptors[0] X509Certificate x509cert = keyDescriptor.keyInfo.x509Datas[0].x509Certificates[0] @@ -421,7 +426,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { //Get second revision - SPSSODescriptor spssoDescriptor_second = entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) + SPSSODescriptor spssoDescriptor_second = EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) KeyDescriptor keyDescriptor_second1 = spssoDescriptor_second.keyDescriptors[0] X509Certificate x509cert_second1 = keyDescriptor_second1.keyInfo.x509Datas[0].x509Certificates[0] @@ -431,7 +436,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { //Get initial revision spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) keyDescriptor = spssoDescriptor.keyDescriptors[0] x509cert = keyDescriptor.keyInfo.x509Datas[0].x509Certificates[0] @@ -475,7 +480,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) AssertionConsumerService acs = spssoDescriptor.assertionConsumerServices[0] then: @@ -500,12 +505,12 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor2 = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,1)) def (acs1, acs2) = [spssoDescriptor2.assertionConsumerServices[0], spssoDescriptor2.assertionConsumerServices[1]] //Initial revision spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory,0)) acs = spssoDescriptor.assertionConsumerServices[0] then: @@ -543,7 +548,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) SingleLogoutService slo = spssoDescriptor.singleLogoutServices[0] then: @@ -565,12 +570,12 @@ class EntityDescriptorEnversVersioningTests extends Specification { entityManager) SPSSODescriptor spssoDescriptor2 = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) def (slo1, slo2) = [spssoDescriptor2.singleLogoutServices[0], spssoDescriptor2.singleLogoutServices[1]] //Initial revision spssoDescriptor = - entityDescriptorService.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) slo = spssoDescriptor.singleLogoutServices[0] then: @@ -608,7 +613,7 @@ class EntityDescriptorEnversVersioningTests extends Specification { txMgr, entityManager) - EntityAttributes attrs = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + EntityAttributes attrs = EntityDescriptorConversionUtils.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) then: entityDescriptorHistory.size() == 1 @@ -628,10 +633,10 @@ class EntityDescriptorEnversVersioningTests extends Specification { txMgr, entityManager) - EntityAttributes attrs2 = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) + EntityAttributes attrs2 = EntityDescriptorConversionUtils.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 1)) //Initial revision - attrs = entityDescriptorService.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) + attrs = EntityDescriptorConversionUtils.getEntityAttributes(getTargetEntityForRevisionIndex(entityDescriptorHistory, 0)) expectedModifiedPersistentEntities = [EntityDescriptor.name, EntityAttributes.name, diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index c34d8e200..b9006e56a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -48,7 +48,7 @@ import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolversPositionOrderContainerService; import edu.internet2.tier.shibboleth.admin.util.AttributeUtility; -import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils; +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils; import edu.internet2.tier.shibboleth.admin.util.LuceneUtility; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; @@ -211,9 +211,9 @@ public FileWritingService fileWritingService() { } @Bean - public EntityDescriptorConverstionUtils EntityDescriptorConverstionUtilsInit(EntityService entityService, OpenSamlObjects oso) { - EntityDescriptorConverstionUtils.setEntityService(entityService); - EntityDescriptorConverstionUtils.setOpenSamlObjects(oso); - return new EntityDescriptorConverstionUtils(); + public EntityDescriptorConversionUtils EntityDescriptorConverstionUtilsInit(EntityService entityService, OpenSamlObjects oso) { + EntityDescriptorConversionUtils.setEntityService(entityService); + EntityDescriptorConversionUtils.setOpenSamlObjects(oso); + return new EntityDescriptorConversionUtils(); } } 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 1874e6d36..a3bd133f8 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 @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.model; +import java.util.HashSet; import java.util.Set; import java.util.UUID; @@ -11,9 +12,6 @@ import javax.persistence.OneToMany; import javax.persistence.Transient; -import org.hibernate.envers.Audited; -import org.hibernate.envers.RelationTargetAuditMode; - import com.fasterxml.jackson.annotation.JsonIgnore; import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; @@ -23,15 +21,6 @@ @Entity(name = "user_groups") @Data public class Group { - public Group() { - } - - public Group(User user) { - resourceId=user.getUsername(); - name=user.getUsername(); - description="default user-group"; - } - @Transient @JsonIgnore public static Group ADMIN_GROUP; @@ -39,20 +28,43 @@ public Group(User user) { @Column(name = "group_description", nullable = true) String description; + @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JsonIgnore + @EqualsAndHashCode.Exclude + Set entityDescriptors = new HashSet<>(); + @Column(nullable = false) String name; @Id @Column(name = "resource_id") String resourceId = UUID.randomUUID().toString(); - - @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, fetch = FetchType.EAGER) - @JsonIgnore + + @OneToMany(mappedBy = "group", fetch = FetchType.EAGER) @EqualsAndHashCode.Exclude - Set users; - - @OneToMany(mappedBy = "group", cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JsonIgnore - @EqualsAndHashCode.Exclude - Set entityDescriptors; + private Set userGroups = new HashSet<>(); + + public Group() { + } + + public Group(User user) { + resourceId = user.getUsername(); + name = user.getUsername(); + description = "default user-group"; + } + + public void addUser(User user) { + if (userGroups == null) { + userGroups = new HashSet<>(); + } + userGroups.add(new UserGroup(this, user)); + } + + public Set getUserGroups() { + if (userGroups == null) { + userGroups = new HashSet<>(); + } + return userGroups; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java index 978c910e8..2e44957ac 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/User.java @@ -17,8 +17,7 @@ import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; -import javax.persistence.ManyToOne; -import javax.persistence.OneToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Transient; import java.util.HashSet; @@ -40,18 +39,18 @@ public class User extends AbstractAuditable { private String emailAddress; private String firstName; - - @ManyToOne - @JoinColumn(name = "group_resource_id") - @EqualsAndHashCode.Exclude - private Group group; @Transient @EqualsAndHashCode.Exclude private String groupId; // simplifies the ui/api - + private String lastName; + @Transient + @JsonIgnore + @EqualsAndHashCode.Exclude + private Set oldUserGroups = new HashSet<>(); + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) @Column(nullable = false) private String password; @@ -66,16 +65,29 @@ public class User extends AbstractAuditable { @EqualsAndHashCode.Exclude private Set roles = new HashSet<>(); + @OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @EqualsAndHashCode.Exclude + private Set userGroups = new HashSet<>(); + @Column(nullable = false, unique = true) private String username; - public Group getGroup() { - return group; + public void clearOldUserGroups() { + oldUserGroups.clear(); } + /** + * @return the initial implementation, while supporting a user having multiple groups in the db side, acts as if the + * user can only belong to a single group + */ + public Group getGroup() { + + return userGroups.isEmpty() ? null : ((UserGroup)userGroups.toArray()[0]).getGroup(); + } + public String getGroupId() { if (groupId == null) { - groupId = group == null ? null : getGroup().getResourceId(); + groupId = userGroups.isEmpty() ? null : getGroup().getResourceId(); } return groupId; } @@ -84,18 +96,55 @@ public String getRole() { if (StringUtils.isBlank(this.role)) { Set roles = this.getRoles(); if (roles.size() != 1) { - throw new RuntimeException(String.format("User with username [%s] does not have exactly one role!", this.getUsername())); + throw new RuntimeException(String.format("User with username [%s] has no role or does not have exactly one role!", this.getUsername())); } this.role = roles.iterator().next().getName(); } return this.role; } + + public Set getUserGroups() { + if (userGroups == null) { + userGroups = new HashSet<>(); + } + return userGroups; + } /** - * If (for some reason) the incoming group is null, the user is defaulted to their own group + * If we change groups, we have to manually manage the set of UserGroups so that we don't have group associations + * we didn't intend (thanks JPA!!). */ public void setGroup(Group assignedGroup) { - this.group = assignedGroup; - this.groupId = getGroup().getResourceId(); + // if the incoming group is the current group, make no changes to our sets + if (assignedGroup.getResourceId().equals(groupId)) { + userGroups.forEach(ug -> { + if (ug.getGroup().getResourceId().equals(groupId)) { + ug.setGroup(assignedGroup); + } + }); + return; + } + + // stash the current groups for removal + getUserGroups().forEach(g -> { + // If the assignedGroup is in the current list, don't bother putting it in the "delete" list + if (!g.getGroup().equals(assignedGroup)) { + oldUserGroups.add(g); + } + }); + userGroups.clear(); + + // Assign the new group + UserGroup ug = new UserGroup(assignedGroup, this); + userGroups.add(ug); + + // Set reference for the UI + groupId = assignedGroup.getResourceId(); + } + + public void setGroups(Set groups) { + oldUserGroups.addAll(getUserGroups()); + getUserGroups().clear(); + groups.forEach(g -> userGroups.add(new UserGroup(g, this))); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserGroup.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserGroup.java new file mode 100644 index 000000000..492fc600b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserGroup.java @@ -0,0 +1,35 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +import javax.persistence.CascadeType; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; + +import lombok.Data; + +@Entity +@Data +public class UserGroup { + public UserGroup() { + } + + public UserGroup(Group group, User user) { + this.group = group; + this.user = user; + } + + @EmbeddedId + UserGroupKey id = new UserGroupKey(); + + @ManyToOne + @MapsId("resourceId") + @JoinColumn(name = "resource_id") + Group group; + + @ManyToOne + @MapsId("userId") + @JoinColumn(name = "user_id") + User user; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserGroupKey.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserGroupKey.java new file mode 100644 index 000000000..d5408f3ef --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/model/UserGroupKey.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.model; + +import java.io.Serializable; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +import lombok.Data; + +@Embeddable +@Data +public class UserGroupKey implements Serializable { + private static final long serialVersionUID = 1L; + + @Column(name = "group_resource_id") + private String resourceId; + + @Column(name = "user_id") + private long userId; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java index 9576184e4..7c3c6e7b1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepository.java @@ -7,6 +7,8 @@ import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; public interface GroupsRepository extends JpaRepository { + void deleteByResourceId(String resourceId); + List findAll(); Group findByResourceId(String id); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserGroupRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserGroupRepository.java new file mode 100644 index 000000000..aba60176c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/repository/UserGroupRepository.java @@ -0,0 +1,18 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.model.UserGroup; +import edu.internet2.tier.shibboleth.admin.ui.security.model.UserGroupKey; + +public interface UserGroupRepository extends JpaRepository { + + List findAllByUser(User user); + + Optional> findAllByGroup(Group group); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java index 040db27aa..939cce661 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceImpl.java @@ -1,24 +1,29 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; import java.util.List; - -import javax.transaction.Transactional; +import java.util.Optional; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; 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.security.model.UserGroup; import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserGroupRepository; @Service public class GroupServiceImpl implements IGroupService, InitializingBean { @Autowired private GroupsRepository repo; + @Autowired + private UserGroupRepository userGroupRepo; + public GroupServiceImpl() { } @@ -27,6 +32,13 @@ public GroupServiceImpl(GroupsRepository repo) { } @Override + public void clearAllForTesting() { + repo.deleteAll(); + afterPropertiesSet(); + } + + @Override + @Transactional public Group createGroup(Group group) throws GroupExistsConflictException { Group foundGroup = find(group.getResourceId()); // If already defined, we don't want to create a new one, nor do we want this call update the definition @@ -39,9 +51,11 @@ public Group createGroup(Group group) throws GroupExistsConflictException { } @Override + @Transactional public void deleteDefinition(String resourceId) throws EntityNotFoundException, GroupDeleteException { Group g = find(resourceId); - if (!g.getUsers().isEmpty() || !g.getEntityDescriptors().isEmpty()) { + Optional> userGroups = userGroupRepo.findAllByGroup(g); + if (userGroups.isEmpty() || !g.getEntityDescriptors().isEmpty()) { throw new GroupDeleteException(String.format( "Unable to delete group with resource id: [%s] - remove all users and entities from group first", resourceId)); @@ -50,6 +64,7 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException, } @Override + @Transactional public Group find(String resourceId) { return repo.findByResourceId(resourceId); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java index 9d9a51f87..1554558e9 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/IGroupService.java @@ -9,6 +9,8 @@ public interface IGroupService { + void clearAllForTesting(); + Group createGroup(Group group) throws GroupExistsConflictException; void deleteDefinition(String resourceId) throws EntityNotFoundException, GroupDeleteException; 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 25ca49f46..dd3bad1b1 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 @@ -1,24 +1,23 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; -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.security.model.Role; -import edu.internet2.tier.shibboleth.admin.ui.security.model.User; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; -import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.DependsOn; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import javax.annotation.PostConstruct; -import javax.transaction.Transactional; +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.security.model.Role; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserGroupRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; @Service public class UserService implements InitializingBean { @@ -30,7 +29,13 @@ public class UserService implements InitializingBean { @Autowired private UserRepository userRepository; + + @Autowired + UserGroupRepository userGroupRepository; + public UserService() { + } + /** * Primarily for testing purposes so we can control the injections */ @@ -108,22 +113,52 @@ public boolean isAuthorizedFor(String objectGroupResourceId) { } /** - * Creating users should always have a group. If the user isn't assigned to a group, create one + * Creating users should always have a group. If the user isn't assigned to a group, create one based on their name. + * If the user has the ADMIN role, they are always solely assigned to the admin group. + * Finally, if the user has multiple groups, that came from an outside auth source, so we want to maintain that list + * (note that if they have the admin role, we will override any group list with the single ADMIN GROUP) */ + @Transactional public User save(User user) { - if (user.getRole().equalsIgnoreCase("ROLE_ADMIN")) { - user.setGroup(Group.ADMIN_GROUP); - } - else if (user.getGroupId() == null) { - Group g = new Group(user); - try { - g = groupService.createGroup(g); - } - catch (GroupExistsConflictException e) { - g = groupService.find(user.getUsername()); + if (user.getUserGroups().size() < 2) { + Group g; + if (user.getRole().equalsIgnoreCase("ROLE_ADMIN")) { + g = groupService.find(Group.ADMIN_GROUP.getResourceId()); + } else if (user.getGroupId() == null) { // Find or create the "user's default" group + g = new Group(user); + try { + g = groupService.createGroup(g); + } + catch (GroupExistsConflictException e) { + g = groupService.find(user.getUsername()); + } + } else { + g = groupService.find(user.getGroupId()); } user.setGroup(g); + } else { + user.getUserGroups().forEach(ug -> { + Group g = groupService.find(ug.getGroup().getResourceId()); + if (g == null) { + try { + Group newGroup = ug.getGroup(); + newGroup.addUser(user); + g = groupService.createGroup(newGroup); + } + catch (GroupExistsConflictException e) { + // we just checked, this shouldn't happen + g = ug.getGroup(); + } + } + ug.setGroup(g); + }); } + // Cleanup any group changes before saving new state + user.getOldUserGroups().forEach(userGroup -> { + userGroupRepository.delete(userGroup); + }); + user.clearOldUserGroups(); + return userRepository.save(user); } 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 236622255..fa4844601 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 @@ -79,7 +79,7 @@ import javax.annotation.PostConstruct; import javax.transaction.Transactional; -import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils.*; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*; import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.*; @Slf4j diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConverstionUtils.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java similarity index 98% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConverstionUtils.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java index 843567496..29e9a9336 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConverstionUtils.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/util/EntityDescriptorConversionUtils.java @@ -1,8 +1,8 @@ package edu.internet2.tier.shibboleth.admin.util; -import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils.getEntityAttributes; -import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils.getOptionalEntityAttributes; -import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils.getSPSSODescriptorFromEntityDescriptor; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.getEntityAttributes; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.getOptionalEntityAttributes; +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.getSPSSODescriptorFromEntityDescriptor; import java.util.Arrays; import java.util.Collections; @@ -52,7 +52,7 @@ import lombok.Setter; @Service -public class EntityDescriptorConverstionUtils { +public class EntityDescriptorConversionUtils { @Autowired @Setter private static OpenSamlObjects openSamlObjects; 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 b7822e373..70affd608 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 @@ -27,7 +27,7 @@ import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator -import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import groovy.json.JsonOutput import groovy.json.JsonSlurper @@ -163,8 +163,8 @@ class EntityDescriptorControllerTests extends Specification { userService.save(user) entityManager.flush() - EntityDescriptorConverstionUtils.setOpenSamlObjects(openSamlObjects) - EntityDescriptorConverstionUtils.setEntityService(entityService) + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setEntityService(entityService) } @Rollback diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy index 19d0e4689..7c1acc5da 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy @@ -16,7 +16,7 @@ import edu.internet2.tier.shibboleth.admin.ui.service.FileCheckingFileWritingSer import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator -import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan @@ -58,7 +58,7 @@ class EntityDescriptorFilesScheduledTasksTests extends Specification { def setup() { randomGenerator = new RandomGenerator() tempPath = tempPath + randomGenerator.randomRangeInt(10000, 20000) - EntityDescriptorConverstionUtils.setOpenSamlObjects(openSamlObjects) + EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) entityDescriptorFilesScheduledTasks = new EntityDescriptorFilesScheduledTasks(tempPath, entityDescriptorRepository, openSamlObjects, new FileCheckingFileWritingService()) directory = new File(tempPath) directory.mkdir() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy index 43efd3315..3c7a7de2f 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupsControllerIntegrationTests.groovy @@ -1,47 +1,102 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +import javax.persistence.EntityManager -import groovy.json.JsonOutput import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc -import org.springframework.boot.test.context.SpringBootTest +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.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.DirtiesContext import org.springframework.test.annotation.Rollback -import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.result.MockMvcResultHandlers -import org.springframework.transaction.annotation.Transactional +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +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.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupDeleteException import edu.internet2.tier.shibboleth.admin.ui.security.exception.GroupExistsConflictException -import spock.lang.Ignore +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.GroupsRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import groovy.json.JsonOutput import spock.lang.Specification -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.patch -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.status - -import javax.persistence.EntityManager - -@SpringBootTest -@AutoConfigureMockMvc -@ActiveProfiles(["no-auth", "dev"]) -@Transactional +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, TestConfiguration, InternationalizationConfiguration, SearchConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext class GroupsControllerIntegrationTests extends Specification { @Autowired - private MockMvc mockMvc + EntityManager entityManager @Autowired - EntityManager entityManager + GroupsRepository groupsRepository + + @Autowired + GroupServiceImpl groupService + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService static RESOURCE_URI = '/api/admin/groups' - static USERS_RESOURCE_URI = '/api/admin/users' + + def MockMvc mockMvc + + def setup() { + def GroupController groupController = new GroupController().with ({ + it.groupService = this.groupService + it + }) + mockMvc = MockMvcBuilders.standaloneSetup(groupController).build(); + + if (roleRepository.count() == 0) { + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + roleRepository.save(it) + } + } + + Optional adminRole = roleRepository.findByName("ROLE_ADMIN") + User adminUser = new User(username: "admin", roles: [adminRole.get()], password: "foo") + userService.save(adminUser) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user = userService.save(user) + entityManager.flush() + } + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) @@ -58,65 +113,90 @@ class GroupsControllerIntegrationTests extends Specification { "resourceId":"FooBar" } """ - when: - def result = mockMvc.perform(post(RESOURCE_URI) - .contentType(MediaType.APPLICATION_JSON) - .content(JsonOutput.toJson(newGroup)) - .accept(MediaType.APPLICATION_JSON)) + def result = mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(newGroup)).accept(MediaType.APPLICATION_JSON)) then: result.andExpect(status().isCreated()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().json(expectedJson, false)) - - when: 'Try to create with an existing resource id' - def exceptionExpected = mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON).content(JsonOutput.toJson(newGroup)) - .accept(MediaType.APPLICATION_JSON)).andReturn().getResolvedException() - - then: 'Expecting method not allowed' - exceptionExpected instanceof GroupExistsConflictException == true + + + //'Try to create with an existing resource id' + try { + mockMvc.perform(post(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(newGroup)) + .accept(MediaType.APPLICATION_JSON)) + 1 == 2 + } catch (Throwable expected) { + expected instanceof GroupExistsConflictException + } } @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'PUT (update) existing group persists properly'() { given: - def group = [name: 'NOT AAA', - description: 'updated AAA', - resourceId: 'AAA'] - - def expectedJson = """ - { - "name":"NOT AAA", - "description":"updated AAA", - "resourceId":"AAA" - } -""" + groupsRepository.deleteByResourceId("AAA") + def Group groupAAA = new Group().with({ + it.name = "AAA" + it.description = "AAA" + it.resourceId = "AAA" + it + }) + groupAAA = groupsRepository.save(groupAAA) + groupAAA.setDescription("Updated AAA") + groupAAA.setName("NOT AAA") + when: - def result = mockMvc.perform(put(RESOURCE_URI) - .contentType(MediaType.APPLICATION_JSON) - .content(JsonOutput.toJson(group)) - .accept(MediaType.APPLICATION_JSON)) + def result = mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(groupAAA)).accept(MediaType.APPLICATION_JSON)) then: result.andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedJson, false)) + .andExpect(jsonPath("\$.name").value("NOT AAA")) + .andExpect(jsonPath("\$.resourceId").value("AAA")) + .andExpect(jsonPath("\$.description").value("Updated AAA")) when: 'Try to update with a non-existing resource id' def newGroup = [name: 'XXXXX', description: 'should not work', resourceId: 'XXXX'] - def exceptionExpected = mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON).content(JsonOutput.toJson(newGroup)) - .accept(MediaType.APPLICATION_JSON)).andReturn().getResolvedException() - - then: 'Expecting nothing happened because the object was not found' - exceptionExpected instanceof EntityNotFoundException == true - } + then: + try { + mockMvc.perform(put(RESOURCE_URI).contentType(MediaType.APPLICATION_JSON) + .content(JsonOutput.toJson(newGroup)) + .accept(MediaType.APPLICATION_JSON)) + 1 == 2 + } catch (Throwable expected) { + expected instanceof EntityNotFoundException + } + } + + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET checks for groups (when there are existing groups)'() { + given: + groupsRepository.deleteByResourceId("AAA") + groupsRepository.deleteByResourceId("BBB") + def Group groupAAA = new Group().with({ + it.name = "AAA" + it.description = "AAA" + it.resourceId = "AAA" + it + }) + groupsRepository.save(groupAAA) + def Group groupBBB = new Group().with({ + it.name = "BBB" + it.description = "BBB" + it.resourceId = "BBB" + it + }) + groupsRepository.save(groupBBB) + when: 'GET request is made for ALL groups in the system, and system has groups in it' def result = mockMvc.perform(get(RESOURCE_URI)) @@ -129,61 +209,47 @@ class GroupsControllerIntegrationTests extends Specification { then: 'GET request for a single specific group completed with HTTP 200' singleGroupRequest.andExpect(status().isOk()) - when: 'GET request for a single non-existent group in a system that has groups' - def exceptionExpected = mockMvc.perform(get("$RESOURCE_URI/CCC")).andReturn().getResolvedException() - - then: 'The group not found' - exceptionExpected instanceof EntityNotFoundException == true + // 'GET request for a single non-existent group in a system that has groups' + try { + mockMvc.perform(get("$RESOURCE_URI/CCC")) + 1 == 2 + } catch (Throwable expected) { + expected instanceof EntityNotFoundException + } } @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'DELETE performs correctly when group attached to a user'() { - given: - def group = [resourceId: 'AAA'] - def newUser = [firstName: 'Foo', - lastName: 'Bar', - username: 'FooBar', - password: 'somepass', - emailAddress: 'foo@institution.edu', - role: 'ROLE_USER', - group: group] + // When the user is created in the setup method above, a new group "someUser" is created to be associated with that user + // User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + // userService.save(user) - when: - def result = mockMvc.perform(post(USERS_RESOURCE_URI) - .contentType(MediaType.APPLICATION_JSON) - .content(JsonOutput.toJson(newUser)) - .accept(MediaType.APPLICATION_JSON)) + when: 'try to delete group that is attached to a user' + def nothingtodo then: - result.andExpect(status().isOk()) - - when: - def userresult = mockMvc.perform(get("$USERS_RESOURCE_URI/$newUser.username")) - def expectedJson = """ -{ - "modifiedBy" : admin, - "firstName" : "Foo", - "emailAddress" : "foo@institution.edu", - "role" : "ROLE_USER", - "username" : "FooBar", - "createdBy" : admin, - "lastName" : "Bar", - "group" : {"resourceId":"AAA"} -}""" - then: 'Request completed with HTTP 200 and returned one user' - userresult.andExpect(status().isOk()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(content().json(expectedJson, false)) + try { + mockMvc.perform(delete("$RESOURCE_URI/someUser")) + 1 == 2 + } catch(Throwable expected) { + expected instanceof GroupDeleteException + } + when: + def Group groupAAA = new Group().with({ + it.name = "AAA" + it.description = "AAA" + it.resourceId = "AAA" + it + }) + groupAAA = groupsRepository.save(groupAAA) - when: 'DELETE request is made' - entityManager.flush() - entityManager.clear() + def User user = userRepository.findByUsername("someUser").get() + user.setGroup(groupAAA) + userService.save(user) - def exceptionExpected = mockMvc.perform(delete("$RESOURCE_URI/$group.resourceId")).andReturn().getResolvedException() - - then: 'Expecting method not allowed' - exceptionExpected instanceof GroupDeleteException == true + then: + mockMvc.perform(delete("$RESOURCE_URI/someUser")) } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy new file mode 100644 index 000000000..d5b0d3620 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/UserServiceTests.groovy @@ -0,0 +1,187 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service + +import javax.persistence.EntityManager + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.PropertySource +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional + +import edu.internet2.tier.shibboleth.admin.ui.ShibbolethUiApplication +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +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.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.model.UserGroup +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserGroupRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import spock.lang.Specification + +@ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration]) +@SpringBootTest(classes = ShibbolethUiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@PropertySource("classpath:application.yml") +@DirtiesContext +class UserServiceTests extends Specification { + + @Autowired + EntityManager entityManager + + @Autowired + IGroupService groupService + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserGroupRepository userGroupRepository + + @Autowired + UserService userService + + @Transactional + def setup() { + // ensure we start fresh with only expected users and roles and groups + userRepository.deleteAll() + roleRepository.deleteAll() + groupService.clearAllForTesting() //leaves us just the admingroup + + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }, new Role().with { + name = 'ROLE_USER' + it + }, new Role().with { + name = 'ROLE_NONE' + it + }] + roles.each { + roleRepository.save(it) + } + } + + @Rollback + def "When creating user, user is set to the correct group"() { + given: + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + Optional userRole = roleRepository.findByName("ROLE_USER") + def User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) + + when: + def result = userService.save(user) + + then: + result.groupId == "testingGroupBBB" + result.username == "someUser" + result.userGroups.size() == 1 + result.getUserGroups().getAt(0).id.resourceId != null + result.getUserGroups().getAt(0).id.userId != 0 + + Group g = groupService.find("testingGroupBBB"); + g.userGroups.size() == 1 + g.getUserGroups().getAt(0).id.resourceId != null + g.getUserGroups().getAt(0).id.userId != 0 + } + + @Rollback + def "When updating user, user is set to the correct group"() { + given: + Group ga = new Group() + ga.setResourceId("testingGroup") + ga.setName("Group A") + ga = groupService.createGroup(ga) + + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + Optional userRole = roleRepository.findByName("ROLE_USER") + def User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) + def User userInB = userService.save(user) + + when: + userInB.setGroup(ga) + def User result = userService.save(user) + def List usersGroups = userGroupRepository.findAllByUser(result) + + then: + usersGroups.size() == 1 + usersGroups.get(0).group.getResourceId() == "testingGroup" + + result.groupId == "testingGroup" + result.username == "someUser" + result.userGroups.size() == 1 + result.getUserGroups().getAt(0).id.resourceId != null + result.getUserGroups().getAt(0).id.userId != 0 + + Group g = groupService.find("testingGroup"); + g.userGroups.size() == 1 + g.getUserGroups().getAt(0).id.resourceId != null + g.getUserGroups().getAt(0).id.userId != 0 + } + + @Rollback + def "When creating user, user with multiple groups is saved correctly"() { + given: + Group ga = new Group() + ga.setResourceId("testingGroup") + ga.setName("Group A") + ga = groupService.createGroup(ga) + + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb = groupService.createGroup(gb) + + Optional userRole = roleRepository.findByName("ROLE_USER") + User user = new User().with( { + it.username = "someUser" + it.roles = [userRole.get()] + it.password = "foo" + it + }) + + HashSet groups = new HashSet<>() + groups.add(ga) + groups.add(gb) + user.setGroups(groups) + + when: + def result = userService.save(user) + + then: + result.userGroups.size() == 2 + + when: + def userFromDb = userRepository.findById(result.id).get(); + + then: + userFromDb.getUserGroups().size() == 2 + + when: + Group gbUpdated = groupService.find("testingGroupBBB") + + then: + gbUpdated.userGroups.size() == 1 + } +} 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 e20b5d8f8..6ceae1eef 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 @@ -9,7 +9,7 @@ import org.springframework.test.context.ActiveProfiles import spock.lang.Specification /** - * Tests for AdminUserService + * Tests for AdminUserService (well, really it tests that the DevConfig worked as much as anything) * * @author Dmitriy Kopylenko */ @@ -20,13 +20,6 @@ class AdminUserServiceTests extends Specification { @Autowired AdminUserService adminUserService - @Autowired - RoleRepository adminRoleRepository - - @Autowired - UserRepository adminUserRepository - - def "Loading existing admin user with admin role"() { given: 'Valid user with admin role is available (loaded by Spring Boot Listener in dev profile)' def user = adminUserService.loadUserByUsername('admin') 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 4ec0b37b8..63a5a133b 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 @@ -27,7 +27,7 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService 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.AttributeUtility -import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import org.opensaml.saml.ext.saml2mdattr.EntityAttributes import org.skyscreamer.jsonassert.JSONAssert @@ -70,8 +70,8 @@ class JPAEntityDescriptorServiceImplTests extends Specification { JacksonTester.initFields(this, mapper) generator = new RandomGenerator() testObjectGenerator = new TestObjectGenerator() - EntityDescriptorConverstionUtils.openSamlObjects = openSamlObjects - EntityDescriptorConverstionUtils.entityService = entityService + EntityDescriptorConversionUtils.openSamlObjects = openSamlObjects + EntityDescriptorConversionUtils.entityService = entityService } def "simple Entity Descriptor"() { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy index 3b65d4f75..371b07409 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests2.groovy @@ -45,7 +45,12 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { UserService userService @Transactional - def setup() { + def setup() { + // ensure we start fresh with only expected users and roles and groups + userRepository.deleteAll() + roleRepository.deleteAll() + groupService.clearAllForTesting() + Group ga = new Group() ga.setResourceId("testingGroup") ga.setName("Group A") @@ -56,8 +61,6 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { gb.setName("Group BBB") gb = groupService.createGroup(gb) - userRepository.deleteAll() // ensure we start fresh with users and roles - roleRepository.deleteAll() def roles = [new Role().with { name = 'ROLE_ADMIN' it diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy index 8360bf8d5..93857449e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/UserBootstrapTests.groovy @@ -7,6 +7,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.ShibUIConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -33,6 +34,9 @@ class UserBootstrapTests extends Specification { @Autowired RoleRepository roleRepository + + @Autowired + UserService userService def setup() { roleRepository.deleteAll(); @@ -42,7 +46,7 @@ class UserBootstrapTests extends Specification { setup: shibUIConfiguration.roles = [] shibUIConfiguration.userBootstrapResource = new ClassPathResource('/conf/1044.csv') - def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + def userBootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository, userService) when: userBootstrap.bootstrapUsersAndRoles(null) @@ -56,7 +60,7 @@ class UserBootstrapTests extends Specification { def "bootstrap roles"() { setup: shibUIConfiguration.roles = ['ROLE_ADMIN', 'ROLE_USER'] - def userbootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository) + def userbootstrap = new UserBootstrap(shibUIConfiguration, userRepository, roleRepository, userService) when: userbootstrap.bootstrapUsersAndRoles(null) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy index 03bbefd77..d7f7a427b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/EntityDescriptorConversionUtilsTests.groovy @@ -25,26 +25,24 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects -import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConverstionUtils +import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll +import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.* class EntityDescriptorConversionUtilsTests extends Specification { @Shared def OpenSamlObjects openSAMLObjects - @Shared - def EntityDescriptorConverstionUtils utilsUnderTest - def setup() { openSAMLObjects = new OpenSamlObjects().with { it.init() it } - utilsUnderTest = new EntityDescriptorConverstionUtils().with { + def EntityDescriptorConversionUtils utilsUnderTest = new EntityDescriptorConversionUtils().with { it.openSamlObjects = openSAMLObjects it } @@ -63,7 +61,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = EntityDescriptorConverstionUtils.createKeyDescriptor('testName', 'signing', 'testValue') + def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'signing', 'testValue') then: assert keyDescriptor == expected @@ -82,7 +80,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected.name = 'testName' when: - def keyDescriptor = EntityDescriptorConverstionUtils.createKeyDescriptor('testName', 'both', 'testValue') + def keyDescriptor = EntityDescriptorConversionUtils.createKeyDescriptor('testName', 'both', 'testValue') def x = openSAMLObjects.marshalToXmlString(keyDescriptor) then: assert keyDescriptor == expected @@ -90,8 +88,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { def 'test createKeyDescriptor equality'() { when: - def key1 = EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test') - def key2 = EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test') + def key1 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') + def key2 = EntityDescriptorConversionUtils.createKeyDescriptor('test', 'signing', 'test') then: assert key1.equals(key2) @@ -101,7 +99,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { def "test #method(#description)"() { setup: expected.setResourceId(starter.getResourceId()) - EntityDescriptorConverstionUtils."$method"(starter, representation) + EntityDescriptorConversionUtils."$method"(starter, representation) expect: assert starter == expected @@ -112,6 +110,11 @@ class EntityDescriptorConversionUtilsTests extends Specification { static class Data { static def getData(OpenSamlObjects openSAMLObjects) { + EntityDescriptorConversionUtils utilsUnderTest = new EntityDescriptorConversionUtils().with { + it.openSamlObjects = openSAMLObjects + it + } + def data = [] data << new DataField( @@ -604,7 +607,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor( + utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -628,7 +632,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) it } ) @@ -637,8 +641,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -661,8 +665,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -671,7 +675,7 @@ class EntityDescriptorConversionUtilsTests extends Specification { expected: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test2', 'encryption', 'test2')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test2', 'encryption', 'test2')) it } ) @@ -691,8 +695,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) it } ) @@ -712,8 +716,8 @@ class EntityDescriptorConversionUtilsTests extends Specification { starter: openSAMLObjects.buildDefaultInstanceOfType(EntityDescriptor.class).with { it.getRoleDescriptors().add( openSAMLObjects.buildDefaultInstanceOfType(SPSSODescriptor.class).with { - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'signing', 'test')) - it.addKeyDescriptor(EntityDescriptorConverstionUtils.createKeyDescriptor('test', 'encryption', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'signing', 'test')) + it.addKeyDescriptor(utilsUnderTest.createKeyDescriptor('test', 'encryption', 'test')) it } )