From 1c4fe322e44c97be73fc5b155dde23cee6f93a53 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Mon, 16 Aug 2021 11:32:09 -0700 Subject: [PATCH 01/48] SHIBUI-1743 Creating branch for feature work --- .../tier/shibboleth/admin/ui/security/model/Group.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 8ccd02b1b..47b607a87 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 @@ -46,13 +46,16 @@ public class Group implements Owner { @Column(name = "resource_id") private String resourceId = UUID.randomUUID().toString(); + @Column(name = "validation_regex") + private String validationRegex = "/*"; + /** * Define a Group object based on the user */ public Group(User user) { resourceId = user.getUsername(); name = user.getUsername(); - description = "default user-group"; + description = "default user-group";new String().matches(""); } @Override From c7a3d3634354bdb804345944dfe5f7c890200871 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Mon, 16 Aug 2021 15:30:04 -0700 Subject: [PATCH 02/48] SHIBUI-2023 Adding regex field to Groups --- .../security/controller/GroupController.java | 7 +- .../GroupControllerExceptionHandler.java | 13 +- .../exception/InvalidGroupRegexException.java | 7 + .../admin/ui/security/model/Group.java | 7 +- .../ui/security/service/GroupServiceImpl.java | 37 ++++- .../ui/security/service/IGroupService.java | 7 +- .../ui/security/service/UserService.java | 8 +- .../security/service/GroupServiceTests.groovy | 148 ++++++++++++++++++ .../security/service/UserServiceTests.groovy | 2 +- 9 files changed, 215 insertions(+), 21 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/InvalidGroupRegexException.java create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java index c0fc0a8ea..bcb1b047c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupController.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -32,7 +33,7 @@ public class GroupController { @Secured("ROLE_ADMIN") @PostMapping @Transactional - public ResponseEntity create(@RequestBody Group group) throws GroupExistsConflictException { + public ResponseEntity create(@RequestBody Group group) throws GroupExistsConflictException, InvalidGroupRegexException { Group result = groupService.createGroup(group); return ResponseEntity.status(HttpStatus.CREATED).body(result); } @@ -64,8 +65,8 @@ public ResponseEntity getOne(@PathVariable String resourceId) throws EntityNo @Secured("ROLE_ADMIN") @PutMapping @Transactional - public ResponseEntity update(@RequestBody Group group) throws EntityNotFoundException { + public ResponseEntity update(@RequestBody Group group) throws EntityNotFoundException, InvalidGroupRegexException { Group result = groupService.updateGroup(group); return ResponseEntity.ok(result); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java index e8bb3b4af..39778e21a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/GroupControllerExceptionHandler.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.security.controller; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -43,4 +44,14 @@ public ResponseEntity handleGroupExistsConflict(GroupExistsConflictException return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).headers(headers) .body(new ErrorResponse(String.valueOf(HttpStatus.METHOD_NOT_ALLOWED.value()), e.getMessage())); } -} + + @ExceptionHandler({ InvalidGroupRegexException.class }) + public ResponseEntity handleInvalidGroupRegexException(InvalidGroupRegexException e, WebRequest request) { + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(ServletUriComponentsBuilder.fromCurrentServletMapping().path("/api/admin/groups").build().toUri()); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.BAD_REQUEST.value()), e.getMessage())); + } + +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/InvalidGroupRegexException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/InvalidGroupRegexException.java new file mode 100644 index 000000000..ae9a034d1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/exception/InvalidGroupRegexException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.exception; + +public class InvalidGroupRegexException extends Exception { + public InvalidGroupRegexException(String message) { + super(message); + } +} \ 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 47b607a87..6a986040c 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 @@ -23,6 +23,8 @@ @EntityListeners(GroupUpdatedEntityListener.class) @Entity(name = "user_groups") public class Group implements Owner { + public static final String DEFAULT_REGEX = "/(?:)/"; //non-capturing + @Transient @JsonIgnore public static Group ADMIN_GROUP; @@ -47,7 +49,7 @@ public class Group implements Owner { private String resourceId = UUID.randomUUID().toString(); @Column(name = "validation_regex") - private String validationRegex = "/*"; + private String validationRegex = DEFAULT_REGEX; /** * Define a Group object based on the user @@ -55,7 +57,8 @@ public class Group implements Owner { public Group(User user) { resourceId = user.getUsername(); name = user.getUsername(); - description = "default user-group";new String().matches(""); + description = "default user-group"; + validationRegex = DEFAULT_REGEX; } @Override 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 2501f007e..7f461c816 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,18 +1,20 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service; -import java.util.List; - -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.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository; import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository; import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.regex.Pattern; @Service @NoArgsConstructor @@ -30,7 +32,7 @@ public GroupServiceImpl(GroupsRepository repo, OwnershipRepository ownershipRepo @Override @Transactional - public Group createGroup(Group group) throws GroupExistsConflictException { + public Group createGroup(Group group) throws GroupExistsConflictException, InvalidGroupRegexException { 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 if (foundGroup != null) { @@ -38,6 +40,7 @@ public Group createGroup(Group group) throws GroupExistsConflictException { String.format("Call update (PUT) to modify the group with resource id: [%s] and name: [%s]", foundGroup.getResourceId(), foundGroup.getName())); } + validateGroupRegex(group); return groupRepository.save(group); } @@ -61,6 +64,7 @@ public void ensureAdminGroupExists() { g = new Group(); g.setName("ADMIN-GROUP"); g.setResourceId("admingroup"); + g.setValidationRegex("/*"); // Everything g = groupRepository.save(g); } Group.ADMIN_GROUP = g; @@ -78,12 +82,29 @@ public List findAll() { } @Override - public Group updateGroup(Group group) throws EntityNotFoundException { + public Group updateGroup(Group group) throws EntityNotFoundException, InvalidGroupRegexException { Group g = find(group.getResourceId()); if (g == null) { throw new EntityNotFoundException(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 missing, go with the default "anything goes" regex, otherwise validate that the pattern is valid to use. + */ + private void validateGroupRegex(Group group) throws InvalidGroupRegexException { + if (StringUtils.isEmpty(group.getValidationRegex())) { + group.setValidationRegex(Group.DEFAULT_REGEX); + return; + } + try { + Pattern.compile(group.getValidationRegex()); + } + catch (Exception e) { + throw new InvalidGroupRegexException("Invalid Regular Expression [ " + group.getValidationRegex() + " ]"); + } + } } \ No newline at end of file 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 1c4229d84..95314123f 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 @@ -6,11 +6,12 @@ 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.exception.InvalidGroupRegexException; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; public interface IGroupService { - Group createGroup(Group group) throws GroupExistsConflictException; + Group createGroup(Group group) throws GroupExistsConflictException, InvalidGroupRegexException; void deleteDefinition(String resourceId) throws EntityNotFoundException, GroupDeleteException; @@ -20,6 +21,6 @@ public interface IGroupService { List findAll(); - Group updateGroup(Group g) throws EntityNotFoundException; + Group updateGroup(Group g) throws EntityNotFoundException, InvalidGroupRegexException; -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index a04e2b574..f6ae4dfd8 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 @@ -4,6 +4,7 @@ import java.util.Optional; import java.util.Set; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContextHolder; @@ -147,7 +148,8 @@ public User save(User user) { try { g = groupService.createGroup(g); } - catch (GroupExistsConflictException e) { + catch (GroupExistsConflictException | InvalidGroupRegexException e) { + // Invalid shouldn't happen for a group created this way. g = groupService.find(user.getUsername()); } } else { @@ -165,8 +167,8 @@ public User save(User user) { Ownership o = ownershipRepository.saveAndFlush(new Ownership(newGroup, user)); g = groupService.createGroup(newGroup); } - catch (GroupExistsConflictException e) { - // we just checked, this shouldn't happen + catch (GroupExistsConflictException | InvalidGroupRegexException e) { + // this shouldn't happen g = ug; } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy new file mode 100644 index 000000000..057cfb576 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy @@ -0,0 +1,148 @@ +package edu.internet2.tier.shibboleth.admin.ui.security.service + +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.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.controller.GroupController +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException +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.OwnershipRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.servlet.LocaleResolver +import org.springframework.web.servlet.i18n.SessionLocaleResolver +import spock.lang.Specification + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, InternationalizationConfiguration, SearchConfiguration, LocalConfig, edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext +@ActiveProfiles(["local"]) +class GroupServiceTests extends Specification { + @Autowired + GroupServiceForTesting groupService + + @Autowired + RoleRepository roleRepository + + @Autowired + UserService userService + + @Transactional + def setup() { + groupService.ensureAdminGroupExists() +// Group g = new Group() +// g.setResourceId("twitter") +// g.setName("twitter") +// // This is valid for a url with "twitter" in it +// g.setValidationRegex("") +// g = groupService.createGroup(g) + + 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") + userService.save(user) + } + + def "Test the validation for regex works"() { + given: + Group g = new Group() + g.setResourceId("twitter") + g.setName("twitter") + g.setValidationRegex(null) + + when: + try { + g = groupService.createGroup(g) + } catch (Exception shouldNotOccur) { + false + } + + then: + g.getValidationRegex() == Group.DEFAULT_REGEX + + when: + g.setValidationRegex("/*") + try { + g = groupService.updateGroup(g) + } catch (Exception shouldNotOccur) { + false + } + + then: + g.getValidationRegex() == "/*" + + when: + g.setValidationRegex("^(http:\\\\/\\\\/www\\\\.|https:\\\\/\\\\/www\\\\.|http:\\\\/\\\\/|https:\\\\/\\\\/)?[a-z0-9]+([\\\\-\\\\.]twitter+)\\\\.[a-z]{2,5}(:[0-9]{1,5})?(\\\\/.*)?\\\$") + try { + g = groupService.updateGroup(g) + } catch (Exception shouldNotOccur) { + false + } + + then: + g.getValidationRegex() == "^(http:\\\\/\\\\/www\\\\.|https:\\\\/\\\\/www\\\\.|http:\\\\/\\\\/|https:\\\\/\\\\/)?[a-z0-9]+([\\\\-\\\\.]twitter+)\\\\.[a-z]{2,5}(:[0-9]{1,5})?(\\\\/.*)?\\\$" + + when: + g.setValidationRegex("*") + + then: + try { + g = groupService.updateGroup(g) + false + } catch (InvalidGroupRegexException shouldOccur) { + true + } + } + + @TestConfiguration + @Profile("local") + static class LocalConfig { + @Bean + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + } +} \ No newline at end of file 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 index b6431f79f..2ef663fbc 100644 --- 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 @@ -258,7 +258,7 @@ class UserServiceTests extends Specification { gbUpdated.ownedItems.size() == 1 } - @org.springframework.boot.test.context.TestConfiguration + @TestConfiguration @Profile("local") static class LocalConfig { @Bean From 86066712f55759b1f42210103e78a657a45aaaa5 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Mon, 16 Aug 2021 15:38:24 -0700 Subject: [PATCH 03/48] SHIBUI-2023 Adding regex field to Groups --- .../MigrationTasksContextLoadedListener.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java index f14d35763..5e6e64b6c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/auto/MigrationTasksContextLoadedListener.java @@ -1,5 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration.auto; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.security.exception.InvalidGroupRegexException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; @@ -14,6 +16,8 @@ import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import java.util.List; + /** * After the context loads, do any needed migration tasks */ @@ -62,6 +66,17 @@ private void doshibui_1740_migration() { userService.save(user); // this will ensure group is set as the default user group } }); - + + // SHIBUI-1743: Adding regex expression to groups + groupService.findAll().forEach(g -> { + g.setValidationRegex(Group.DEFAULT_REGEX); + try { + groupService.updateGroup(g); + } + catch (Exception e) { + // Shouldn't happen + e.printStackTrace(); + } + }); } -} +} \ No newline at end of file From 3d324018ae023bd563b6158c3763e89d428ba2fb Mon Sep 17 00:00:00 2001 From: chasegawa Date: Mon, 16 Aug 2021 19:42:21 -0700 Subject: [PATCH 04/48] SHIBUI-2024 Added validation when saving entity descriptor - entity ID and ACS locations will validate against the group regex --- .../EntityDescriptorController.java | 9 +- ...yDescriptorControllerExceptionHandler.java | 30 +-- .../exception/InvalidUrlMatchException.java | 7 + .../ui/security/service/GroupServiceImpl.java | 12 +- .../ui/security/service/IGroupService.java | 1 + .../ui/service/EntityDescriptorService.java | 19 +- .../JPAEntityDescriptorServiceImpl.java | 88 ++++---- .../EntityDescriptorControllerTests.groovy | 200 +++++++++++------- ...PAEntityDescriptorServiceImplTests2.groovy | 12 +- 9 files changed, 234 insertions(+), 144 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java 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 f8e28fc75..ff0e3732c 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 @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.User; @@ -73,7 +74,8 @@ public EntityDescriptorController(EntityDescriptorVersionService versionService) @PostMapping("/EntityDescriptor") @Transactional - public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException { + public ResponseEntity create(@RequestBody EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } @@ -145,7 +147,8 @@ public void initRestTemplate() { @PutMapping("/EntityDescriptor/{resourceId}") @Transactional - public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException { + public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) + throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException, InvalidUrlMatchException { edRepresentation.setId(resourceId); // This should be the same already, but just to be safe... EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation); return ResponseEntity.ok().body(result); @@ -171,4 +174,4 @@ public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam .body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage())); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java index d0b18f415..7eae86cb5 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java @@ -1,7 +1,9 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; -import java.util.ConcurrentModificationException; - +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; +import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; +import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -10,9 +12,7 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; -import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; -import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import java.util.ConcurrentModificationException; @ControllerAdvice(assignableTypes = {EntityDescriptorController.class}) public class EntityDescriptorControllerExceptionHandler extends ResponseEntityExceptionHandler { @@ -21,24 +21,30 @@ public class EntityDescriptorControllerExceptionHandler extends ResponseEntityEx public ResponseEntity handleConcurrentModificationException(ConcurrentModificationException e, WebRequest request) { return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage())); } - + @ExceptionHandler({ EntityIdExistsException.class }) public ResponseEntity handleEntityExistsException(EntityIdExistsException e, WebRequest request) { HttpHeaders headers = new HttpHeaders(); headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage())); - return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse( - String.valueOf(HttpStatus.CONFLICT.value()), - String.format("The entity descriptor with entity id [%s] already exists.", e.getMessage()))); + return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers) + .body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()), + String.format("The entity descriptor with entity id [%s] already exists.", + e.getMessage()))); } - + @ExceptionHandler({ EntityNotFoundException.class }) public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage())); } - + @ExceptionHandler({ ForbiddenException.class }) public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest request) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage())); } -} + + @ExceptionHandler({ InvalidUrlMatchException.class }) + public ResponseEntity handleInvalidUrlMatchException(InvalidUrlMatchException e, WebRequest request) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage())); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java new file mode 100644 index 000000000..ea2d463c7 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InvalidUrlMatchException extends Exception { + public InvalidUrlMatchException(String message) { + super(message); + } +} \ No newline at end of file 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 7f461c816..bcca4c088 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 @@ -56,6 +56,16 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException, groupRepository.delete(group); } + /** + * Though the name URI is used here, any string value that we want to validate against the group's regex is accepted and checked. + * Designed usage is that this would be a URL or an entity Id (which is a URI that does not have to follow the URL conventions) + */ + @Override + public boolean doesUrlMatchGroupPattern(String groupId, String uri) { + Group group = find(groupId); + return Pattern.matches(group.getValidationRegex(), uri); + } + @Override @Transactional public void ensureAdminGroupExists() { @@ -64,7 +74,7 @@ public void ensureAdminGroupExists() { g = new Group(); g.setName("ADMIN-GROUP"); g.setResourceId("admingroup"); - g.setValidationRegex("/*"); // Everything + g.setValidationRegex("^.+$"); // Just about everything g = groupRepository.save(g); } Group.ADMIN_GROUP = g; 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 95314123f..d95415127 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 @@ -23,4 +23,5 @@ public interface IGroupService { Group updateGroup(Group g) throws EntityNotFoundException, InvalidGroupRegexException; + boolean doesUrlMatchGroupPattern(String groupId, String uri); } \ No newline at end of file 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 bc63e7378..54fe65ddd 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 @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; import java.util.ConcurrentModificationException; import java.util.List; @@ -31,7 +32,8 @@ public interface EntityDescriptorService { * @throws ForbiddenException If user is unauthorized to perform this operation * @throws EntityIdExistsException If any EntityDescriptor already exists with the same EntityId */ - EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptor ed) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException; /** * @param edRepresentation Incoming representation to save @@ -39,7 +41,8 @@ public interface EntityDescriptorService { * @throws ForbiddenException If user is unauthorized to perform this operation * @throws EntityIdExistsException If the entity already exists */ - EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException; + EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException; /** * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor @@ -93,13 +96,13 @@ public interface EntityDescriptorService { Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList); /** - * @param edRepresentation Incoming representation to save - * @return EntityDescriptorRepresentation - * @throws ForbiddenException If user is unauthorized to perform this operation - * @throws EntityIdExistsException If the entity already exists - * @throws ConcurrentModificationException If the entity was already modified by another user + * @throws ForbiddenException If the user is not permitted to perform the action + * @throws EntityNotFoundException If the entity doesn't already exist in the database + * @throws ConcurrentModificationException IF the entity is being modified in another session + * @throws InvalidUrlMatchException If the entity id or the ACS location urls don't match the supplied regex */ - EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException; + EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) + throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException, InvalidUrlMatchException; /** * Update an instance of entity descriptor with information from the front-end representation 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 9e0f7117e..84c956d93 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 @@ -1,24 +1,11 @@ package edu.internet2.tier.shibboleth.admin.ui.service; -import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributes; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor; -import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty; -import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean; -import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation; -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation; -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.domain.*; +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.*; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; @@ -29,23 +16,14 @@ import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; import lombok.extern.slf4j.Slf4j; - -import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; -import javax.transaction.Transactional; - import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.*; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues; @Slf4j @Service @@ -94,12 +72,14 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptor ed) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { return createNew(createRepresentationFromDescriptor(ed)); } @Override - public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException { + public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) + throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } @@ -107,9 +87,15 @@ public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation e if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) { throw new EntityIdExistsException(edRep.getEntityId()); } - + + // "Create new" will use the current user's group as the owner + String ownerId = userService.getCurrentUserGroup().getOwnerId(); + edRep.setIdOfOwner(ownerId); + validateEntityIdAndACSUrls(edRep); + EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep); - ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId()); + ed.setIdOfOwner(ownerId); + return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed)); } @@ -331,14 +317,14 @@ public void delete(String resourceId) throws ForbiddenException, EntityNotFoundE } ownershipRepository.deleteEntriesForOwnedObject(ed); entityDescriptorRepository.delete(ed); - + } @Override public Iterable getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException { if (!userService.currentUserIsAdmin()) { throw new ForbiddenException(); - } + } return entityDescriptorRepository.findAllDisabledAndNotOwnedByAdmin().map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList()); } @@ -371,31 +357,34 @@ public EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throw } if (!userService.isAuthorizedFor(ed)) { throw new ForbiddenException(); - } + } return ed; } - + @Override public Map getRelyingPartyOverridesRepresentationFromAttributeList(List attributeList) { return ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList(attributeList); } - + @Override - public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityNotFoundException { + public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) + throws ForbiddenException, EntityNotFoundException, InvalidUrlMatchException { EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId()); if (existingEd == null) { - throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); + throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); } if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } if (!userService.isAuthorizedFor(existingEd)) { throw new ForbiddenException(); - } + } // Verify we're the only one attempting to update the EntityDescriptor if (edRep.getVersion() != existingEd.hashCode()) { - throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId())); + throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId())); } + + validateEntityIdAndACSUrls(edRep); updateDescriptorFromRepresentation(existingEd, edRep); return createRepresentationFromDescriptor(entityDescriptorRepository.save(existingEd)); } @@ -407,4 +396,21 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. } buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); } -} + + private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidUrlMatchException { + // Check the entity id first + if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { + throw new InvalidUrlMatchException("EntityId is not a pattern match to the group"); + } + + // Check the ACS locations + if (edRep.getAssertionConsumerServices() != null && edRep.getAssertionConsumerServices().size() > 0) { + for (AssertionConsumerServiceRepresentation acs : edRep.getAssertionConsumerServices()) { + if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), acs.getLocationUrl())) { + throw new InvalidUrlMatchException( + "ACS location [ " + acs.getLocationUrl() + " ] is not a pattern match to the group"); + } + } + } + } +} \ No newline at end of file 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 a04c9ebba..717aeb155 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 @@ -6,9 +6,12 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.Internationalization 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.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.security.model.Group @@ -65,7 +68,7 @@ import org.springframework.web.client.RestTemplate import org.springframework.web.servlet.config.annotation.EnableWebMvc import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver - +import org.springframework.web.util.NestedServletException import spock.lang.Ignore import spock.lang.Shared import spock.lang.Specification @@ -137,7 +140,15 @@ class EntityDescriptorControllerTests extends Specification { @Transactional def setup() { + groupService.clearAllForTesting() groupService.ensureAdminGroupExists() + + Group gb = new Group(); + gb.setResourceId("testingGroupBBB") + gb.setName("Group BBB") + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + gb = groupService.createGroup(gb) + generator = new TestObjectGenerator() randomGenerator = new RandomGenerator() mapper = new ObjectMapper() @@ -177,6 +188,7 @@ class EntityDescriptorControllerTests extends Specification { Optional userRole = roleRepository.findByName("ROLE_USER") User user = new User(username: "someUser", roles:[userRole.get()], password: "foo") + user.setGroup(gb) userService.save(user) EntityDescriptorConversionUtils.setOpenSamlObjects(openSamlObjects) @@ -190,22 +202,22 @@ class EntityDescriptorControllerTests extends Specification { authentication.getName() >> 'admin' def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false) entityDescriptorRepository.save(entityDescriptor) - + when: 'pre-check' entityManager.flush() - + then: entityDescriptorRepository.findAll().size() == 1 - + when: def result = mockMvc.perform(delete("/api/EntityDescriptor/uuid-1")) - + then: result.andExpect(status().isNoContent()) entityDescriptorRepository.findByResourceId("uuid-1") == null entityDescriptorRepository.findAll().size() == 0 } - + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptors with empty repository as admin'() { @@ -230,17 +242,17 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptors with 1 record in repository as admin'() { given: authentication.getName() >> 'admin' - - def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") + + def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") entityDescriptorRepository.saveAndFlush(entityDescriptor) - + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() - when: + when: def result = mockMvc.perform(get('/api/EntityDescriptors')) - then: + then: def mvcResult = result.andExpect(expectedHttpResponseStatus).andExpect(content().contentType(expectedResponseContentType)) .andExpect(jsonPath("\$.[0].id").value("uuid-1")) .andExpect(jsonPath("\$.[0].entityId").value("eid1")) @@ -253,14 +265,14 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptors with 2 records in repository as admin'() { given: authentication.getName() >> 'admin' - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: "admingroup") - + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) - + def expectedResponseContentType = APPLICATION_JSON def expectedHttpResponseStatus = status().isOk() @@ -278,20 +290,73 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.[1].entityId").value("eid2")) .andExpect(jsonPath("\$.[1].serviceEnabled").value(false)) .andExpect(jsonPath("\$.[1].idOfOwner").value("admingroup")) - } - + } + + @Rollback + @WithMockUser(value = "someUser", roles = ["USER"]) + def 'POST create new - verifying validation on entityID and ACS locations'() { + given: + authentication.getName() >> 'someUser' + + def expectedEntityId = 'https://shib.org/blah/blah' + EntityDescriptorRepresentation edRep = new EntityDescriptorRepresentation() + edRep.setEntityId(expectedEntityId) + edRep.setServiceProviderName("spName") + + def acsList = new ArrayList() + AssertionConsumerServiceRepresentation acsRep = new AssertionConsumerServiceRepresentation() + acsRep.setIndex(0) + acsRep.setLocationUrl("http://logout.shib.org/dologout") + acsList.add(acsRep) + edRep.setAssertionConsumerServices(acsList) + + def edRepJson = mapper.writeValueAsString(edRep) + + when: + def result = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("\$.entityId").value("https://shib.org/blah/blah")) + .andExpect(jsonPath("\$.serviceEnabled").value(false)) + .andExpect(jsonPath("\$.idOfOwner").value("testingGroupBBB")) + + when: "ACS url is bad" + expectedEntityId = 'https://shib.org/blah/blah/again' + edRep = new EntityDescriptorRepresentation() + edRep.setEntityId(expectedEntityId) + edRep.setServiceProviderName("spName") + + acsList = new ArrayList() + acsRep = new AssertionConsumerServiceRepresentation() + acsRep.setIndex(0) + acsRep.setLocationUrl("http://shib.com/dologout") + acsList.add(acsRep) + edRep.setAssertionConsumerServices(acsList) + edRepJson = mapper.writeValueAsString(edRep) + + then: + try { + mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) + false + } catch (NestedServletException expected) { + expected.getCause() instanceof InvalidUrlMatchException + } + } + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'POST /EntityDescriptor and successfully create new record'() { given: - authentication.getName() >> 'admin' - + authentication.getName() >> 'admin' + def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' def expectedResponseHeader = 'Location' def expectedResponseHeaderValue = "/api/EntityDescriptor/" - def postedJsonBody = """ + def postedJsonBody = """ { "serviceProviderName": "$expectedSpName", "entityId": "$expectedEntityId", @@ -305,7 +370,7 @@ class EntityDescriptorControllerTests extends Specification { "securityInfo": null, "assertionConsumerServices": null, "current": false - } + } """ when: @@ -324,13 +389,13 @@ class EntityDescriptorControllerTests extends Specification { def 'POST /EntityDescriptor as user disallows enabling'() { given: authentication.getName() >> 'someUser' - + def expectedEntityId = 'https://shib' def expectedSpName = 'sp1' when: def postedJsonBody = """ - { + { "serviceProviderName": "$expectedSpName", "entityId": "$expectedEntityId", "organization": null, @@ -346,9 +411,9 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null - } + } """ - + then: try { def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -364,7 +429,7 @@ class EntityDescriptorControllerTests extends Specification { given: authentication.getName() >> 'admin' - def postedJsonBody = """ + def postedJsonBody = """ { "serviceProviderName": "sp1", "entityId": "eid1", @@ -381,17 +446,17 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null - } + } """ when: def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false) - + entityDescriptorRepository.save(entityDescriptorOne) entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() - + then: try { def exceptionExpected = mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -400,7 +465,7 @@ class EntityDescriptorControllerTests extends Specification { e instanceof EntityIdExistsException == true } } - + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptor/{resourceId} non-existent'() { @@ -415,7 +480,7 @@ class EntityDescriptorControllerTests extends Specification { e instanceof EntityNotFoundException == true } } - + @Rollback @WithMockUser(value = "admin", roles = ["ADMIN"]) def 'GET /EntityDescriptor/{resourceId} existing'() { @@ -424,10 +489,10 @@ class EntityDescriptorControllerTests extends Specification { def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "admingroup") entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() - + when: - def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) - + def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) + then: result.andExpect(status().isOk()) .andExpect(jsonPath("\$.entityId").value("eid1")) @@ -442,17 +507,17 @@ class EntityDescriptorControllerTests extends Specification { given: authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: "someUser") def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) - + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) - + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) - - + + when: def result = mockMvc.perform(get("/api/EntityDescriptor/uuid-1")) @@ -470,13 +535,13 @@ class EntityDescriptorControllerTests extends Specification { when: authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) - + entityDescriptorRepository.saveAndFlush(entityDescriptorOne) entityDescriptorRepository.saveAndFlush(entityDescriptorTwo) - + ownershipRepository.saveAndFlush(new Ownership(g, entityDescriptorOne)) ownershipRepository.saveAndFlush(new Ownership(Group.ADMIN_GROUP, entityDescriptorTwo)) @@ -486,7 +551,7 @@ class EntityDescriptorControllerTests extends Specification { } catch (Exception e) { e instanceof ForbiddenException == true - } + } } @Rollback @@ -494,7 +559,7 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing (xml)'() { given: authentication.getName() >> 'admin' - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) entityDescriptorOne.setElementLocalName("EntityDescriptor") entityDescriptorOne.setNamespacePrefix("md") entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") @@ -518,7 +583,7 @@ class EntityDescriptorControllerTests extends Specification { given: authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) entityDescriptorOne.setElementLocalName("EntityDescriptor") entityDescriptorOne.setNamespacePrefix("md") @@ -543,14 +608,14 @@ class EntityDescriptorControllerTests extends Specification { when: authentication.getName() >> 'someUser' Group g = Group.ADMIN_GROUP - + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) entityDescriptorOne.setElementLocalName("EntityDescriptor") entityDescriptorOne.setNamespacePrefix("md") entityDescriptorOne.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() - + then: try { def exceptionExpected = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId").accept(APPLICATION_XML)) @@ -565,7 +630,7 @@ class EntityDescriptorControllerTests extends Specification { def "POST /EntityDescriptor handles XML happily"() { given: authentication.getName() >> 'admin' - + def postedBody = ''' @@ -599,7 +664,7 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(jsonPath("\$.serviceEnabled").value(false)) .andExpect(jsonPath("\$.idOfOwner").value("admingroup")) .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.protocolSupportEnum").value("SAML 2")) - .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.nameIdFormats[0]").value("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")) + .andExpect(jsonPath("\$.serviceProviderSsoDescriptor.nameIdFormats[0]").value("urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified")) .andExpect(jsonPath("\$.assertionConsumerServices[0].binding").value("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")) .andExpect(jsonPath("\$.assertionConsumerServices[0].makeDefault").value(false)) .andExpect(jsonPath("\$.assertionConsumerServices[0].locationUrl").value("https://test.scaldingspoon.org/test1/acs")) @@ -633,11 +698,11 @@ class EntityDescriptorControllerTests extends Specification { def spName = randomGenerator.randomString() def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true) def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'http://test.scaldingspoon.org/test1', serviceProviderName: 'sp2', serviceEnabled: false) - + entityDescriptorRepository.save(entityDescriptorOne) entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() - + then: try { def exceptionExpected = mockMvc.perform(post("/api/EntityDescriptor").contentType(APPLICATION_XML).content(postedBody).param("spName", spName)) @@ -652,9 +717,9 @@ class EntityDescriptorControllerTests extends Specification { def "PUT /EntityDescriptor updates entity descriptors properly as admin"() { given: authentication.getName() >> 'admin' - + def entityDescriptorTwo = new EntityDescriptor(resourceId: 'uuid-2', entityID: 'eid2', serviceProviderName: 'sp2', serviceEnabled: false, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) - + entityDescriptorTwo = entityDescriptorRepository.save(entityDescriptorTwo) entityManager.flush() entityManager.clear() @@ -662,7 +727,7 @@ class EntityDescriptorControllerTests extends Specification { def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorTwo) updatedEntityDescriptorRepresentation.setServiceProviderName("newName") def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) - + when: def result = mockMvc.perform(put("/api/EntityDescriptor/uuid-2").contentType(APPLICATION_JSON).content(postedJsonBody)) @@ -676,12 +741,12 @@ class EntityDescriptorControllerTests extends Specification { @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) - def "PUT /EntityDescriptor disallows non-admin user from enabling"() { + def "PUT /EntityDescriptor disallows non-admin user from enabling"() { given: authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: g.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: false, idOfOwner: g.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() @@ -703,12 +768,12 @@ class EntityDescriptorControllerTests extends Specification { @Rollback @WithMockUser(value = "someUser", roles = ["USER"]) - def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { + def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { given: authentication.getName() >> 'someUser' Group g = userService.getCurrentUserGroup() - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: g.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() @@ -733,8 +798,8 @@ class EntityDescriptorControllerTests extends Specification { def "PUT /EntityDescriptor throws a concurrent mod exception if the version numbers don't match"() { given: authentication.getName() >> 'admin' - - def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) + + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, idOfOwner: Group.ADMIN_GROUP.getOwnerId()) entityDescriptorOne = entityDescriptorRepository.save(entityDescriptorOne) entityManager.flush() @@ -742,7 +807,7 @@ class EntityDescriptorControllerTests extends Specification { entityDescriptorOne.serviceProviderName = 'foo' entityDescriptorOne.resourceId = 'uuid-1' def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(entityDescriptorOne) - + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) then: @@ -768,13 +833,4 @@ class EntityDescriptorControllerTests extends Specification { return result } } -} - -//when: -//def Set ownerships = ownershipRepository.findOwnableObjectOwners(ed) -// -//then: -//ownerships.size() == 1 -//ownerships.each { -// it.ownerId == groupFromDb.resourceId -//} \ No newline at end of file +} \ No newline at end of file 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 13847472c..906bc4a19 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 @@ -63,11 +63,12 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { Group ga = new Group() ga.setResourceId("testingGroup") ga.setName("Group A") - ga = groupService.createGroup(ga) + groupService.createGroup(ga) - Group gb = new Group(); + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") + gb.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") gb = groupService.createGroup(gb) def roles = [new Role().with { @@ -100,12 +101,9 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { User current = userService.getCurrentUser() current.setGroupId("testingGroupBBB") - def expectedCreationDate = '2017-10-23T11:11:11' - def expectedEntityId = 'https://shib' + def expectedEntityId = 'https://shib.org/blah' def expectedSpName = 'sp1' def expectedUUID = 'uuid-1' - def expectedResponseHeader = 'Location' - def expectedResponseHeaderValue = "/api/EntityDescriptor/$expectedUUID" def entityDescriptor = new EntityDescriptor(resourceId: expectedUUID, entityID: expectedEntityId, serviceProviderName: expectedSpName, serviceEnabled: false) when: @@ -128,4 +126,4 @@ class JPAEntityDescriptorServiceImplTests2 extends Specification { return result } } -} +} \ No newline at end of file From c4e1b63c53381b302a29f03e19d472a78f5ea847 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 17 Aug 2021 13:24:46 -0700 Subject: [PATCH 05/48] SHIBUI-2024 Added validation for FileBackedHttpMetadataResolver url --- ...DurationIMetadataResolverValidator.groovy} | 9 +- .../JPAMetadataResolverServiceImpl.groovy | 6 +- ...tadataResolverValidationConfiguration.java | 30 +++-- .../MetadataResolversController.java | 6 +- ...leBackedHttpMetadataResolverValidator.java | 32 +++++ .../IMetadataResolverValidator.java} | 8 +- .../MetadataResolverValidationService.java | 16 ++- ...urceBackedIMetadataResolverValidator.java} | 9 +- ...dataResolverValidationConfiguration.groovy | 26 ++-- ...dHttpMetadataResolverValidatorTests.groovy | 124 ++++++++++++++++++ ...ValidationServiceConfigurationTests.groovy | 10 +- ...adataResolverValidationServiceTests.groovy | 19 +-- ...esourceBackedMetadataValidatorTests.groovy | 20 ++- .../security/service/GroupServiceTests.groovy | 6 - 14 files changed, 252 insertions(+), 69 deletions(-) rename backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/{DurationMetadataResolverValidator.groovy => validator/DurationIMetadataResolverValidator.groovy} (78%) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java rename backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/{MetadataResolverValidator.java => validator/IMetadataResolverValidator.java} (82%) rename backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/{ => validator}/MetadataResolverValidationService.java (59%) rename backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/{ResourceBackedMetadataResolverValidator.java => validator/ResourceBackedIMetadataResolverValidator.java} (57%) create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidatorTests.groovy rename backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/{ => validator}/MetadataResolverValidationServiceConfigurationTests.groovy (81%) rename backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/{ => validator}/MetadataResolverValidationServiceTests.groovy (86%) rename backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/{ => validator}/ResourceBackedMetadataValidatorTests.groovy (54%) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DurationMetadataResolverValidator.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationIMetadataResolverValidator.groovy similarity index 78% rename from backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DurationMetadataResolverValidator.groovy rename to backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationIMetadataResolverValidator.groovy index 56a2ecd77..ef7c70f23 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DurationMetadataResolverValidator.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationIMetadataResolverValidator.groovy @@ -1,8 +1,11 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.util.DurationUtility -class DurationMetadataResolverValidator implements MetadataResolverValidator { +class DurationIMetadataResolverValidator implements IMetadataResolverValidator { boolean supports(MetadataResolver resolver) { return resolver.hasProperty('dynamicMetadataResolverAttributes') || resolver.hasProperty('reloadableMetadataResolverAttributes') } @@ -27,4 +30,4 @@ class DurationMetadataResolverValidator implements MetadataResolverValidator { } return new ValidationResult() } -} +} \ No newline at end of file diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index 7ffb5b5f7..bea96989f 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -400,7 +400,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { disregardTLSCertificate: resolver.httpMetadataResolverAttributes?.disregardTLSCertificate ?: null, httpClientSecurityParametersRef: resolver.httpMetadataResolverAttributes?.httpClientSecurityParametersRef, proxyHost: resolver.httpMetadataResolverAttributes?.proxyHost, - proxyPort: resolver.httpMetadataResolverAttributes?.proxyHost, + proxyPort: resolver.httpMetadataResolverAttributes?.proxyPort, proxyUser: resolver.httpMetadataResolverAttributes?.proxyUser, proxyPassword: resolver.httpMetadataResolverAttributes?.proxyPassword, httpCaching: resolver.httpMetadataResolverAttributes?.httpCaching, @@ -471,7 +471,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { disregardTLSCertificate: resolver.httpMetadataResolverAttributes?.disregardTLSCertificate ?: null, httpClientSecurityParametersRef: resolver.httpMetadataResolverAttributes?.httpClientSecurityParametersRef, proxyHost: resolver.httpMetadataResolverAttributes?.proxyHost, - proxyPort: resolver.httpMetadataResolverAttributes?.proxyHost, + proxyPort: resolver.httpMetadataResolverAttributes?.proxyPort, proxyUser: resolver.httpMetadataResolverAttributes?.proxyUser, proxyPassword: resolver.httpMetadataResolverAttributes?.proxyPassword, httpCaching: resolver.httpMetadataResolverAttributes?.httpCaching, @@ -559,4 +559,4 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java index 86d3f9f58..b5609f60d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java @@ -1,9 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DurationMetadataResolverValidator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.DurationIMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.FileBackedHttpMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -12,19 +15,22 @@ @Configuration public class MetadataResolverValidationConfiguration { + @Bean ResourceBackedIMetadataResolverValidator resourceBackedMetadataResolverValidator() { + return new ResourceBackedIMetadataResolverValidator(); + } + @Bean - ResourceBackedMetadataResolverValidator resourceBackedMetadataResolverValidator() { - return new ResourceBackedMetadataResolverValidator(); + FileBackedHttpMetadataResolverValidator fileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + return new FileBackedHttpMetadataResolverValidator(groupService, userService); } @Bean @SuppressWarnings("Unchecked") - MetadataResolverValidationService metadataResolverValidationService(List metadataResolverValidators) { - return new MetadataResolverValidationService(metadataResolverValidators); + MetadataResolverValidationService metadataResolverValidationService(List IMetadataResolverValidators) { + return new MetadataResolverValidationService(IMetadataResolverValidators); } - @Bean - DurationMetadataResolverValidator durationMetadataResolverValidator() { - return new DurationMetadataResolverValidator(); + @Bean DurationIMetadataResolverValidator durationMetadataResolverValidator() { + return new DurationIMetadataResolverValidator(); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 6bce7af7b..915995d4c 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.exc.InvalidTypeIdException; import edu.internet2.tier.shibboleth.admin.ui.domain.exceptions.MetadataFileNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.versioning.Version; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; @@ -43,7 +43,7 @@ import java.net.URI; import java.util.List; -import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.ValidationResult; @RestController @RequestMapping("/api") @@ -212,4 +212,4 @@ private void doResolverInitialization(MetadataResolver persistedResolver) throws OpenSamlChainingMetadataResolverUtil.updateChainingMetadataResolver((OpenSamlChainingMetadataResolver) chainingMetadataResolver, openSamlRepresentation); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java new file mode 100644 index 000000000..19d165859 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java @@ -0,0 +1,32 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +public class FileBackedHttpMetadataResolverValidator implements IMetadataResolverValidator { + @Autowired + IGroupService groupService; + + @Autowired + UserService userService; + + public FileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + this.groupService = groupService; + this.userService = userService; + } + + @Override public boolean supports(MetadataResolver resolver) { return resolver instanceof FileBackedHttpMetadataResolver; } + + @Override public ValidationResult validate(MetadataResolver resolver) { + FileBackedHttpMetadataResolver fbhmResolver = (FileBackedHttpMetadataResolver) resolver; + String url = fbhmResolver.getMetadataURL(); + if (!groupService.doesUrlMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { + return new ValidationResult("Metadata URL not acceptable for user's group"); + } + return new ValidationResult(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/IMetadataResolverValidator.java similarity index 82% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidator.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/IMetadataResolverValidator.java index a57bc6f18..33ceefa00 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidator.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/IMetadataResolverValidator.java @@ -1,4 +1,6 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import java.util.ArrayList; import java.util.List; @@ -12,7 +14,7 @@ * * @author Dmitriy Kopylenko */ -public interface MetadataResolverValidator { +public interface IMetadataResolverValidator { boolean supports(MetadataResolver resolver); @@ -38,4 +40,4 @@ public boolean isValid() { return this.errorMessages == null || this.errorMessages.isEmpty(); } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationService.java similarity index 59% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationService.java index 676755b26..3e9e3df51 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationService.java @@ -1,13 +1,15 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.ValidationResult; import java.util.ArrayList; import java.util.List; /** - * A facade that aggregates {@link MetadataResolverValidator}s available to call just one of them supporting the type of a given resolver. - * If no {@link MetadataResolverValidator}s are configured, considers provided MetadataResolver as valid. + * A facade that aggregates {@link IMetadataResolverValidator}s available to call just one of them supporting the type of a given resolver. + * If no {@link IMetadataResolverValidator}s are configured, considers provided MetadataResolver as valid. *

* Uses chain-of-responsibility design pattern * @@ -15,9 +17,9 @@ */ public class MetadataResolverValidationService { - private List> validators; + List> validators; - public MetadataResolverValidationService(List> validators) { + public MetadataResolverValidationService(List> validators) { this.validators = validators != null ? validators : new ArrayList<>(); } @@ -36,4 +38,4 @@ public ValidationResult validateIfNecessary(T metadataResolver) { boolean noValidatorsConfigured() { return this.validators.size() == 0; } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedIMetadataResolverValidator.java similarity index 57% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolverValidator.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedIMetadataResolverValidator.java index 480491465..6b4c7ba6d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolverValidator.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedIMetadataResolverValidator.java @@ -1,6 +1,9 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; -public class ResourceBackedMetadataResolverValidator implements MetadataResolverValidator { +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver; + +public class ResourceBackedIMetadataResolverValidator implements IMetadataResolverValidator { @Override public boolean supports(MetadataResolver resolver) { @@ -17,4 +20,4 @@ public ValidationResult validate(ResourceBackedMetadataResolver resolver) { } return new ValidationResult(); } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy index d5c2f4f29..37f69ff8c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy @@ -1,19 +1,29 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolverValidator - +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.FileBackedHttpMetadataResolverValidator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile @Configuration class TestMetadataResolverValidationConfiguration { @Bean - ResourceBackedMetadataResolverValidator resourceBackedMetadataResolverValidator() { - new ResourceBackedMetadataResolverValidator() + @Profile("fbh-test") + FileBackedHttpMetadataResolverValidator fileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + new FileBackedHttpMetadataResolverValidator(groupService, userService) + } + + @Bean + ResourceBackedIMetadataResolverValidator resourceBackedMetadataResolverValidator() { + new ResourceBackedIMetadataResolverValidator() } @Bean @@ -22,8 +32,8 @@ class TestMetadataResolverValidationConfiguration { } @Bean - MetadataResolverValidationService metadataResolverValidationServiceOneValidator(List metadataResolverValidators) { + MetadataResolverValidationService metadataResolverValidationService(List metadataResolverValidators) { new MetadataResolverValidationService(metadataResolverValidators) } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidatorTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidatorTests.groovy new file mode 100644 index 000000000..790baec2d --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidatorTests.groovy @@ -0,0 +1,124 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator + +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.configuration.TestMetadataResolverValidationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +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.OwnershipRepository +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.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional +import spock.lang.Specification + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, TestMetadataResolverValidationConfiguration, LocalConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ActiveProfiles(["fbh-test"]) +class FileBackedHttpMetadataResolverValidatorTests extends Specification { + @Autowired + GroupServiceForTesting groupServiceForTesting + + @Autowired + @Qualifier("metadataResolverValidationService") + MetadataResolverValidationService metadataResolverValidationService + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + + @Transactional + def setup() { + userRepository.deleteAll() + roleRepository.deleteAll() + groupServiceForTesting.clearAllForTesting() + + Group g = new Group() + g.setResourceId("shib") + g.setName("shib") + // This is valid for a url with "shib.org" in it + g.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + g = groupServiceForTesting.createGroup(g) + + 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 userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo", group: g) + userService.save(user) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + @Rollback + def "test validation by service works properly"() { + given: + FileBackedHttpMetadataResolver metadataResolver = new FileBackedHttpMetadataResolver() + metadataResolver.setMetadataURL("http://foo.shib.org/bar") + + when: + IMetadataResolverValidator.ValidationResult result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + result.isValid() + + when: "using a bad url (no match)" + metadataResolver.setMetadataURL("http://foo.shib.com/bar") + result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + !result.isValid() + } + + @org.springframework.boot.test.context.TestConfiguration + @Profile("fbh-test") + static class LocalConfig { + @Bean + @Primary + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceConfigurationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceConfigurationTests.groovy similarity index 81% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceConfigurationTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceConfigurationTests.groovy index 9007cb15e..501afe9b9 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceConfigurationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceConfigurationTests.groovy @@ -1,4 +1,4 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator import edu.internet2.tier.shibboleth.admin.ui.configuration.TestMetadataResolverValidationConfiguration import org.springframework.beans.factory.annotation.Autowired @@ -17,8 +17,8 @@ class MetadataResolverValidationServiceConfigurationTests extends Specification MetadataResolverValidationService metadataResolverValidationServiceNoValidators @Autowired - @Qualifier("metadataResolverValidationServiceOneValidator") - MetadataResolverValidationService metadataResolverValidationServiceOneValidator + @Qualifier("metadataResolverValidationService") + MetadataResolverValidationService metadataResolverValidationService def "Validation service with no validators"() { expect: @@ -27,6 +27,6 @@ class MetadataResolverValidationServiceConfigurationTests extends Specification def "Validation service with one validator"() { expect: - !metadataResolverValidationServiceOneValidator.noValidatorsConfigured() + !metadataResolverValidationService.noValidatorsConfigured() } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy similarity index 86% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy index d62f07170..f0aaf8aa4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/MetadataResolverValidationServiceTests.groovy @@ -1,9 +1,10 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import spock.lang.Specification import spock.lang.Subject -import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.* +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator.* /** * @author Dmitriy Kopylenko @@ -26,7 +27,7 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with one validator not supporting the type of resolver returns default valid result"() { given: 'Sample metadata resolver and validation service with one validator not supporting that type' def resolver = Mock(MetadataResolver) - def validator = Mock(MetadataResolverValidator) + def validator = Mock(IMetadataResolverValidator) validator.supports(_) >> false @Subject def validationService = new MetadataResolverValidationService([validator]) @@ -41,7 +42,7 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with one validator supporting the type of resolver but fails its validation"() { given: 'Sample metadata resolver and validation service with one validator supporting that type' def resolver = Mock(MetadataResolver) - def validator = Mock(MetadataResolverValidator) + def validator = Mock(IMetadataResolverValidator) validator.supports(_) >> true validator.validate(_) >> new ValidationResult('Invalid') @Subject @@ -57,10 +58,10 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with with two validators supporting the type of resolver, first fails, second passes validation"() { given: 'Sample metadata resolver and validation service with two validators supporting that type' def resolver = Mock(MetadataResolver) - def validator1 = Mock(MetadataResolverValidator) + def validator1 = Mock(IMetadataResolverValidator) validator1.supports(_) >> true validator1.validate(_) >> new ValidationResult('Invalid') - def validator2 = Mock(MetadataResolverValidator) + def validator2 = Mock(IMetadataResolverValidator) validator2.supports(_) >> true validator2.validate(_) >> new ValidationResult(null) @Subject @@ -76,9 +77,9 @@ class MetadataResolverValidationServiceTests extends Specification { def "Validation service with with two validators, only one supporting the type of resolver, passes validation"() { given: 'Sample metadata resolver and validation service with two validators, with one supporting that type' def resolver = Mock(MetadataResolver) - def validator1 = Mock(MetadataResolverValidator) + def validator1 = Mock(IMetadataResolverValidator) validator1.supports(_) >> false - def validator2 = Mock(MetadataResolverValidator) + def validator2 = Mock(IMetadataResolverValidator) validator2.supports(_) >> true validator2.validate(_) >> new ValidationResult(null) @Subject @@ -90,4 +91,4 @@ class MetadataResolverValidationServiceTests extends Specification { then: validationResult.valid } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataValidatorTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy similarity index 54% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataValidatorTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy index 6447a7f1a..66102f4fe 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataValidatorTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/ResourceBackedMetadataValidatorTests.groovy @@ -1,12 +1,18 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers - +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator import spock.lang.Specification class ResourceBackedMetadataValidatorTests extends Specification { def "Does not support foreign resolver type"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() FileBackedHttpMetadataResolver resolver = new FileBackedHttpMetadataResolver() expect: @@ -15,7 +21,7 @@ class ResourceBackedMetadataValidatorTests extends Specification { def "Passes validation"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver().with { it.classpathMetadataResource = new ClasspathMetadataResource() it @@ -28,7 +34,7 @@ class ResourceBackedMetadataValidatorTests extends Specification { def "Does not pass validation with both resource types missing"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver() expect: @@ -38,7 +44,7 @@ class ResourceBackedMetadataValidatorTests extends Specification { def "Does not pass validation with both resource types present"() { given: - MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + IMetadataResolverValidator validator = new ResourceBackedIMetadataResolverValidator() ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver().with { it.classpathMetadataResource = new ClasspathMetadataResource() it.svnMetadataResource = new SvnMetadataResource() @@ -49,4 +55,4 @@ class ResourceBackedMetadataValidatorTests extends Specification { validator.supports(resolver) !validator.validate(resolver).valid } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy index 057cfb576..7673a907a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy @@ -48,12 +48,6 @@ class GroupServiceTests extends Specification { @Transactional def setup() { groupService.ensureAdminGroupExists() -// Group g = new Group() -// g.setResourceId("twitter") -// g.setName("twitter") -// // This is valid for a url with "twitter" in it -// g.setValidationRegex("") -// g = groupService.createGroup(g) if (roleRepository.count() == 0) { def roles = [new Role().with { From 015986276baa60a7a78ac22b148a051430075444 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 17 Aug 2021 16:31:42 -0700 Subject: [PATCH 06/48] SHIBUI-2024 Added validation for DynamicHttpMetadataResolver url --- ...tadataResolverValidationConfiguration.java | 10 +- .../DynamicHttpMetadataResolverValidator.java | 31 +++++ ...dataResolverValidationConfiguration.groovy | 7 + ...cHttpMetadataResolverValidatorTests.groovy | 123 ++++++++++++++++++ 4 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidatorTests.groovy diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java index b5609f60d..12c076c42 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java @@ -1,10 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.DurationIMetadataResolverValidator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.FileBackedHttpMetadataResolverValidator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.ResourceBackedIMetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.*; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import org.springframework.context.annotation.Bean; @@ -19,6 +15,10 @@ public class MetadataResolverValidationConfiguration { return new ResourceBackedIMetadataResolverValidator(); } + @Bean DynamicHttpMetadataResolverValidator dynamicHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + return new DynamicHttpMetadataResolverValidator(groupService, userService); + } + @Bean FileBackedHttpMetadataResolverValidator fileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { return new FileBackedHttpMetadataResolverValidator(groupService, userService); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java new file mode 100644 index 000000000..a161ec2fa --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java @@ -0,0 +1,31 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; + +public class DynamicHttpMetadataResolverValidator implements IMetadataResolverValidator { + @Autowired IGroupService groupService; + + @Autowired UserService userService; + + public DynamicHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + this.groupService = groupService; + this.userService = userService; + } + + @Override public boolean supports(MetadataResolver resolver) { return resolver instanceof DynamicHttpMetadataResolver; } + + @Override public ValidationResult validate(MetadataResolver resolver) { + DynamicHttpMetadataResolver dynamicResolver = (DynamicHttpMetadataResolver) resolver; + if ("MetadataQueryProtocol".equals(dynamicResolver.getMetadataRequestURLConstructionScheme().getType())) { + String url = dynamicResolver.getMetadataRequestURLConstructionScheme().getContent(); + if (!groupService.doesUrlMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { + return new ValidationResult("Metadata Query Protocol URL not acceptable for user's group"); + } + } + return new ValidationResult(); + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy index 37f69ff8c..6daa2c816 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.configuration +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.DynamicHttpMetadataResolverValidator import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.MetadataResolverValidationService import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.FileBackedHttpMetadataResolverValidator import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator.IMetadataResolverValidator @@ -15,6 +16,12 @@ import org.springframework.context.annotation.Profile @Configuration class TestMetadataResolverValidationConfiguration { + @Bean + @Profile("dh-test") + DynamicHttpMetadataResolverValidator dynamicHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { + new DynamicHttpMetadataResolverValidator(groupService, userService) + } + @Bean @Profile("fbh-test") FileBackedHttpMetadataResolverValidator fileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidatorTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidatorTests.groovy new file mode 100644 index 000000000..7b4470b18 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidatorTests.groovy @@ -0,0 +1,123 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.validator + +import edu.internet2.tier.shibboleth.admin.ui.configuration.* +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme +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.OwnershipRepository +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.GroupServiceForTesting +import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.Profile +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.security.test.context.support.WithMockUser +import org.springframework.test.annotation.Rollback +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.ContextConfiguration +import org.springframework.transaction.annotation.Transactional +import spock.lang.Specification + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration, TestMetadataResolverValidationConfiguration, LocalConfig]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@ActiveProfiles(["dh-test"]) +class DynamicHttpMetadataResolverValidatorTests extends Specification { + @Autowired + GroupServiceForTesting groupServiceForTesting + + @Autowired + @Qualifier("metadataResolverValidationService") + MetadataResolverValidationService metadataResolverValidationService + + @Autowired + RoleRepository roleRepository + + @Autowired + UserRepository userRepository + + @Autowired + UserService userService + + @Transactional + def setup() { + userRepository.deleteAll() + roleRepository.deleteAll() + groupServiceForTesting.clearAllForTesting() + + Group g = new Group() + g.setResourceId("shib") + g.setName("shib") + // This is valid for a url with "shib.org" in it + g.setValidationRegex("^(?:https?:\\/\\/)?(?:[^.]+\\.)?shib\\.org(\\/.*)?\$") + g = groupServiceForTesting.createGroup(g) + + 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 userRole = roleRepository.findByName("ROLE_USER") + User user = new User(username: "someUser", roles:[userRole.get()], password: "foo", group: g) + userService.save(user) + } + + @WithMockUser(value = "someUser", roles = ["USER"]) + @Rollback + def "test validation by service works properly"() { + given: + DynamicHttpMetadataResolver metadataResolver = new DynamicHttpMetadataResolver() + MetadataQueryProtocolScheme scheme = new MetadataQueryProtocolScheme() + scheme.setContent("http://foo.shib.org/bar") + metadataResolver.setMetadataRequestURLConstructionScheme(scheme) + + when: + IMetadataResolverValidator.ValidationResult result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + result.isValid() + + when: "using a bad url (no match)" + metadataResolver.getMetadataRequestURLConstructionScheme().setContent("http://foo.shib.com/bar") + result = metadataResolverValidationService.validateIfNecessary(metadataResolver) + + then: + !result.isValid() + } + + @org.springframework.boot.test.context.TestConfiguration + @Profile("dh-test") + static class LocalConfig { + @Bean + @Primary + GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { + GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { + it.groupRepository = repo + it.ownershipRepository = ownershipRepository + return it + }) + result.ensureAdminGroupExists() + return result + } + } +} \ No newline at end of file From 963b23f94078dee3ad2d3ab9f6ca6474f42873a6 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 17 Aug 2021 18:25:51 -0700 Subject: [PATCH 07/48] SHIBUI-2024 Added validation for filters urls --- .../controller/MetadataFiltersController.java | 234 ++++++++++-------- .../DynamicHttpMetadataResolverValidator.java | 8 +- ...leBackedHttpMetadataResolverValidator.java | 7 +- .../ui/security/service/GroupServiceImpl.java | 2 +- .../ui/security/service/IGroupService.java | 2 +- .../JPAEntityDescriptorServiceImpl.java | 4 +- ...taFiltersControllerIntegrationTests.groovy | 8 +- 7 files changed, 149 insertions(+), 116 deletions(-) diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java index 33d4458b5..7f48faa1a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java @@ -1,14 +1,12 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityRoleWhiteListFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.NameIdFormatFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.filters.SignatureValidationFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import org.springframework.beans.factory.annotation.Autowired; @@ -37,52 +35,39 @@ import java.util.stream.Stream; import static java.util.stream.Collectors.toList; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; @RestController @RequestMapping("/api/MetadataResolvers/{metadataResolverId}") public class MetadataFiltersController { + private static final Supplier HTTP_400_BAD_REQUEST_EXCEPTION = () -> new HttpClientErrorException(BAD_REQUEST); + private static final Supplier HTTP_404_CLIENT_ERROR_EXCEPTION = () -> new HttpClientErrorException(NOT_FOUND); @Autowired - private MetadataResolverRepository repository; - - @Autowired - private MetadataResolverService metadataResolverService; + org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver; @Autowired private FilterRepository filterRepository; @Autowired - org.opensaml.saml.metadata.resolver.MetadataResolver chainingMetadataResolver; - - private static final Supplier HTTP_404_CLIENT_ERROR_EXCEPTION = () -> new HttpClientErrorException(NOT_FOUND); + private IGroupService groupService; - @ExceptionHandler - public ResponseEntity notFoundHandler(HttpClientErrorException ex) { - if (ex.getStatusCode() == NOT_FOUND) { - return ResponseEntity.notFound().build(); - } - throw ex; - } + @Autowired + private MetadataResolverService metadataResolverService; - @GetMapping("/Filters") - @Transactional(readOnly = true) - public ResponseEntity getAll(@PathVariable String metadataResolverId) { - MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); - return ResponseEntity.ok(resolver.getMetadataFilters()); - } + @Autowired + private MetadataResolverRepository repository; - @GetMapping("/Filters/{resourceId}") - @Transactional(readOnly = true) - public ResponseEntity getOne(@PathVariable String metadataResolverId, @PathVariable String resourceId) { - MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); - return ResponseEntity.ok(findFilterOrThrowHttp404(resourceId)); - } + @Autowired + private UserService userService; @PostMapping("/Filters") @Transactional public ResponseEntity create(@PathVariable String metadataResolverId, @RequestBody MetadataFilter createdFilter) { MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); + validateFilterOrThrowHttp400(createdFilter); + metadataResolver.addFilter(createdFilter); MetadataResolver persistedMr = repository.save(metadataResolver); @@ -92,57 +77,14 @@ public ResponseEntity create(@PathVariable String metadataResolverId, @Reques MetadataFilter persistedFilter = newlyPersistedFilter(persistedMr.getMetadataFilters().stream(), createdFilter.getResourceId()); return ResponseEntity - .created(getResourceUriFor(persistedMr, createdFilter.getResourceId())) - .body(persistedFilter); - } - - @PutMapping("/Filters/{resourceId}") - @Transactional - public ResponseEntity update(@PathVariable String metadataResolverId, - @PathVariable String resourceId, - @RequestBody MetadataFilter updatedFilter) { - - MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); - - //Now we operate directly on the filter attached to MetadataResolver, - //Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many - Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters() - .stream() - .filter(it -> it.getResourceId().equals(resourceId)) - .findFirst(); - if (!filterTobeUpdatedOptional.isPresent()) { - return ResponseEntity.notFound().build(); - } - MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); - if (!resourceId.equals(updatedFilter.getResourceId())) { - return new ResponseEntity(HttpStatus.CONFLICT); - } - - // Verify we're the only one attempting to update the filter - if (updatedFilter.getVersion() != filterTobeUpdated.getVersion()) { - return new ResponseEntity(HttpStatus.CONFLICT); - } - - filterTobeUpdated.setName(updatedFilter.getName()); - filterTobeUpdated.setFilterEnabled(updatedFilter.isFilterEnabled()); - updatedFilter.updateConcreteFilterTypeData(filterTobeUpdated); - - MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); - - //To support envers versioning from MetadataResolver side - metadataResolver.markAsModified(); - repository.save(metadataResolver); - - // TODO: do we need to reload filters here? - reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); - - return ResponseEntity.ok().body(persistedFilter); + .created(getResourceUriFor(persistedMr, createdFilter.getResourceId())) + .body(persistedFilter); } @DeleteMapping("/Filters/{resourceId}") @Transactional public ResponseEntity delete(@PathVariable String metadataResolverId, - @PathVariable String resourceId) { + @PathVariable String resourceId) { MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); MetadataFilter filterToDelete = findFilterOrThrowHttp404(resourceId); @@ -168,6 +110,66 @@ public ResponseEntity delete(@PathVariable String metadataResolverId, return ResponseEntity.noContent().build(); } + private MetadataFilter findFilterOrThrowHttp404(String filterResourceId) { + MetadataFilter filter = filterRepository.findByResourceId(filterResourceId); + if (filter == null) { + throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + } + return filter; + } + + private MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) { + MetadataResolver resolver = repository.findByResourceId(resolverResourceId); + if (resolver == null) { + throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + } + return resolver; + } + + @GetMapping("/Filters") + @Transactional(readOnly = true) + public ResponseEntity getAll(@PathVariable String metadataResolverId) { + MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); + return ResponseEntity.ok(resolver.getMetadataFilters()); + } + + @GetMapping("/Filters/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String metadataResolverId, @PathVariable String resourceId) { + MetadataResolver resolver = findResolverOrThrowHttp404(metadataResolverId); + return ResponseEntity.ok(findFilterOrThrowHttp404(resourceId)); + } + + private static URI getResourceUriFor(MetadataResolver mr, String filterResourceId) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/MetadataResolvers/") + .pathSegment(mr.getResourceId()) + .pathSegment("Filters") + .pathSegment(filterResourceId) + .build() + .toUri(); + } + + private MetadataFilter newlyPersistedFilter(Stream filters, final String filterResourceId) { + MetadataFilter persistedFilter = filters + .filter(f -> f.getResourceId().equals(filterResourceId)) + .collect(toList()).get(0); + + return persistedFilter; + } + + @ExceptionHandler + public ResponseEntity notFoundHandler(HttpClientErrorException ex) { + switch (ex.getStatusCode()) { + case NOT_FOUND: + return ResponseEntity.notFound().build(); + case BAD_REQUEST: + return ResponseEntity.badRequest().build(); + default: + throw ex; + } + } + private void reloadFiltersAndHandleScriptException(String resolverResourceId) { try { metadataResolverService.reloadFilters(resolverResourceId); @@ -182,37 +184,65 @@ private void reloadFiltersAndHandleScriptException(String resolverResourceId) { } } - private MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) { - MetadataResolver resolver = repository.findByResourceId(resolverResourceId); - if (resolver == null) { - throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + @PutMapping("/Filters/{resourceId}") + @Transactional + public ResponseEntity update(@PathVariable String metadataResolverId, + @PathVariable String resourceId, + @RequestBody MetadataFilter updatedFilter) { + + MetadataResolver metadataResolver = findResolverOrThrowHttp404(metadataResolverId); + + //Now we operate directly on the filter attached to MetadataResolver, + //Instead of fetching filter separately, to accommodate correct envers versioning with uni-directional one-to-many + Optional filterTobeUpdatedOptional = metadataResolver.getMetadataFilters() + .stream() + .filter(it -> it.getResourceId().equals(resourceId)) + .findFirst(); + if (!filterTobeUpdatedOptional.isPresent()) { + return ResponseEntity.notFound().build(); + } + MetadataFilter filterTobeUpdated = filterTobeUpdatedOptional.get(); + if (!resourceId.equals(updatedFilter.getResourceId())) { + return new ResponseEntity(HttpStatus.CONFLICT); } - return resolver; - } - private MetadataFilter findFilterOrThrowHttp404(String filterResourceId) { - MetadataFilter filter = filterRepository.findByResourceId(filterResourceId); - if (filter == null) { - throw HTTP_404_CLIENT_ERROR_EXCEPTION.get(); + // Verify we're the only one attempting to update the filter + if (updatedFilter.getVersion() != filterTobeUpdated.getVersion()) { + return new ResponseEntity(HttpStatus.CONFLICT); } - return filter; - } - private MetadataFilter newlyPersistedFilter(Stream filters, final String filterResourceId) { - MetadataFilter persistedFilter = filters - .filter(f -> f.getResourceId().equals(filterResourceId)) - .collect(toList()).get(0); + // perform validation if necessary on the entity ids (if the filter is the right configuration to need such a check) + validateFilterOrThrowHttp400(updatedFilter); - return persistedFilter; + filterTobeUpdated.setName(updatedFilter.getName()); + filterTobeUpdated.setFilterEnabled(updatedFilter.isFilterEnabled()); + updatedFilter.updateConcreteFilterTypeData(filterTobeUpdated); + + MetadataFilter persistedFilter = filterRepository.save(filterTobeUpdated); + + //To support envers versioning from MetadataResolver side + metadataResolver.markAsModified(); + repository.save(metadataResolver); + + // TODO: do we need to reload filters here? + reloadFiltersAndHandleScriptException(metadataResolver.getResourceId()); + + return ResponseEntity.ok().body(persistedFilter); } - private static URI getResourceUriFor(MetadataResolver mr, String filterResourceId) { - return ServletUriComponentsBuilder - .fromCurrentServletMapping().path("/api/MetadataResolvers/") - .pathSegment(mr.getResourceId()) - .pathSegment("Filters") - .pathSegment(filterResourceId) - .build() - .toUri(); + /** + * IF the filter is of type "EntityAttributes" AND the target is "ENTITY" THEN check each of the values (which are entityIds) + */ + private void validateFilterOrThrowHttp400(MetadataFilter createdFilter) { + if ("EntityAttributes".equals(createdFilter.getType())) { + EntityAttributesFilter filter = (EntityAttributesFilter) createdFilter; + if ("ENTITY".equals(filter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType())) { + for (String entityId : filter.getEntityAttributesFilterTarget().getValue()) { + if (!groupService.doesStringMatchGroupPattern(userService.getCurrentUser().getGroupId(), entityId)) { + throw HTTP_400_BAD_REQUEST_EXCEPTION.get(); + } + } + } + } } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java index a161ec2fa..2b14ded1e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DynamicHttpMetadataResolverValidator.java @@ -7,9 +7,11 @@ import org.springframework.beans.factory.annotation.Autowired; public class DynamicHttpMetadataResolverValidator implements IMetadataResolverValidator { - @Autowired IGroupService groupService; + @Autowired + private IGroupService groupService; - @Autowired UserService userService; + @Autowired + private UserService userService; public DynamicHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { this.groupService = groupService; @@ -22,7 +24,7 @@ public DynamicHttpMetadataResolverValidator(IGroupService groupService, UserServ DynamicHttpMetadataResolver dynamicResolver = (DynamicHttpMetadataResolver) resolver; if ("MetadataQueryProtocol".equals(dynamicResolver.getMetadataRequestURLConstructionScheme().getType())) { String url = dynamicResolver.getMetadataRequestURLConstructionScheme().getContent(); - if (!groupService.doesUrlMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { + if (!groupService.doesStringMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { return new ValidationResult("Metadata Query Protocol URL not acceptable for user's group"); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java index 19d165859..e387e9bea 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/FileBackedHttpMetadataResolverValidator.java @@ -2,17 +2,16 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; -import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService; import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import org.springframework.beans.factory.annotation.Autowired; public class FileBackedHttpMetadataResolverValidator implements IMetadataResolverValidator { @Autowired - IGroupService groupService; + private IGroupService groupService; @Autowired - UserService userService; + private UserService userService; public FileBackedHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { this.groupService = groupService; @@ -24,7 +23,7 @@ public FileBackedHttpMetadataResolverValidator(IGroupService groupService, UserS @Override public ValidationResult validate(MetadataResolver resolver) { FileBackedHttpMetadataResolver fbhmResolver = (FileBackedHttpMetadataResolver) resolver; String url = fbhmResolver.getMetadataURL(); - if (!groupService.doesUrlMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { + if (!groupService.doesStringMatchGroupPattern(userService.getCurrentUser().getGroupId(), url)) { return new ValidationResult("Metadata URL not acceptable for user's group"); } return new ValidationResult(); 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 bcca4c088..4749a5401 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 @@ -61,7 +61,7 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException, * Designed usage is that this would be a URL or an entity Id (which is a URI that does not have to follow the URL conventions) */ @Override - public boolean doesUrlMatchGroupPattern(String groupId, String uri) { + public boolean doesStringMatchGroupPattern(String groupId, String uri) { Group group = find(groupId); return Pattern.matches(group.getValidationRegex(), uri); } 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 d95415127..d6e44e5ec 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 @@ -23,5 +23,5 @@ public interface IGroupService { Group updateGroup(Group g) throws EntityNotFoundException, InvalidGroupRegexException; - boolean doesUrlMatchGroupPattern(String groupId, String uri); + boolean doesStringMatchGroupPattern(String groupId, String uri); } \ 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 84c956d93..f6a979e2d 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 @@ -399,14 +399,14 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidUrlMatchException { // Check the entity id first - if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { + if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { throw new InvalidUrlMatchException("EntityId is not a pattern match to the group"); } // Check the ACS locations if (edRep.getAssertionConsumerServices() != null && edRep.getAssertionConsumerServices().size() > 0) { for (AssertionConsumerServiceRepresentation acs : edRep.getAssertionConsumerServices()) { - if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), acs.getLocationUrl())) { + if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), acs.getLocationUrl())) { throw new InvalidUrlMatchException( "ACS location [ " + acs.getLocationUrl() + " ] is not a pattern match to the group"); } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy index e223e0012..41ed176ad 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerIntegrationTests.groovy @@ -20,6 +20,7 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.test.context.ActiveProfiles @@ -32,7 +33,7 @@ import static org.springframework.http.HttpMethod.PUT * @author Dmitriy Kopylenko */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@ActiveProfiles("no-auth") +@ActiveProfiles(["no-auth", "mfci-test"]) class MetadataFiltersControllerIntegrationTests extends Specification { @Autowired @@ -249,7 +250,8 @@ class MetadataFiltersControllerIntegrationTests extends Specification { } @TestConfiguration - static class Config { + @Profile("mfci-test") + static class LocalConfig { @Bean MetadataResolver metadataResolver() { new OpenSamlChainingMetadataResolver().with { @@ -259,4 +261,4 @@ class MetadataFiltersControllerIntegrationTests extends Specification { } } } -} +} \ No newline at end of file From 6a708c5d1dd840b8b698da7593629bba8310bfc7 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 17 Aug 2021 20:33:22 -0700 Subject: [PATCH 08/48] SHIBUI-2024 Reverting misnamed file --- ...idator.groovy => DurationMetadataResolverValidator.groovy} | 2 +- .../MetadataResolverValidationConfiguration.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/{DurationIMetadataResolverValidator.groovy => DurationMetadataResolverValidator.groovy} (95%) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationIMetadataResolverValidator.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationMetadataResolverValidator.groovy similarity index 95% rename from backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationIMetadataResolverValidator.groovy rename to backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationMetadataResolverValidator.groovy index ef7c70f23..85dfc3845 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationIMetadataResolverValidator.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/validator/DurationMetadataResolverValidator.groovy @@ -5,7 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.util.DurationUtility -class DurationIMetadataResolverValidator implements IMetadataResolverValidator { +class DurationMetadataResolverValidator implements IMetadataResolverValidator { boolean supports(MetadataResolver resolver) { return resolver.hasProperty('dynamicMetadataResolverAttributes') || resolver.hasProperty('reloadableMetadataResolverAttributes') } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java index 12c076c42..f41b47eaa 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java @@ -30,7 +30,7 @@ MetadataResolverValidationService metadataResolverValidationService(List Date: Tue, 17 Aug 2021 20:40:56 -0700 Subject: [PATCH 09/48] SHIBUI-2024 slight refactoring of names --- ...tadataResolverValidationConfiguration.java | 8 +++--- .../EntityDescriptorController.java | 16 +++-------- ...yDescriptorControllerExceptionHandler.java | 6 ++-- .../InvalidPatternMatchException.java | 7 +++++ .../exception/InvalidUrlMatchException.java | 7 ----- .../ui/service/EntityDescriptorService.java | 11 ++++---- .../JPAEntityDescriptorServiceImpl.java | 14 +++++----- .../EntityDescriptorControllerTests.groovy | 28 ++----------------- 8 files changed, 33 insertions(+), 64 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidPatternMatchException.java delete mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java index f41b47eaa..fdf63e9e0 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java @@ -11,8 +11,8 @@ @Configuration public class MetadataResolverValidationConfiguration { - @Bean ResourceBackedIMetadataResolverValidator resourceBackedMetadataResolverValidator() { - return new ResourceBackedIMetadataResolverValidator(); + @Bean DurationMetadataResolverValidator durationMetadataResolverValidator() { + return new DurationMetadataResolverValidator(); } @Bean DynamicHttpMetadataResolverValidator dynamicHttpMetadataResolverValidator(IGroupService groupService, UserService userService) { @@ -30,7 +30,7 @@ MetadataResolverValidationService metadataResolverValidationService(List create(@RequestBody EntityDescriptorRepresentation edRepresentation) - throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { + throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException { EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation); return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd); } @@ -148,7 +139,8 @@ public void initRestTemplate() { @PutMapping("/EntityDescriptor/{resourceId}") @Transactional public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) - throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException, InvalidUrlMatchException { + throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException, + InvalidPatternMatchException { edRepresentation.setId(resourceId); // This should be the same already, but just to be safe... EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation); return ResponseEntity.ok().body(result); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java index 7eae86cb5..6ec47a82a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerExceptionHandler.java @@ -3,7 +3,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; -import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -43,8 +43,8 @@ public ResponseEntity handleForbiddenAccess(ForbiddenException e, WebRequest return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage())); } - @ExceptionHandler({ InvalidUrlMatchException.class }) - public ResponseEntity handleInvalidUrlMatchException(InvalidUrlMatchException e, WebRequest request) { + @ExceptionHandler({ InvalidPatternMatchException.class }) + public ResponseEntity handleInvalidUrlMatchException(InvalidPatternMatchException e, WebRequest request) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage())); } } \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidPatternMatchException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidPatternMatchException.java new file mode 100644 index 000000000..57cdfcd42 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidPatternMatchException.java @@ -0,0 +1,7 @@ +package edu.internet2.tier.shibboleth.admin.ui.exception; + +public class InvalidPatternMatchException extends Exception { + public InvalidPatternMatchException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java deleted file mode 100644 index ea2d463c7..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/exception/InvalidUrlMatchException.java +++ /dev/null @@ -1,7 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.exception; - -public class InvalidUrlMatchException extends Exception { - public InvalidUrlMatchException(String message) { - super(message); - } -} \ No newline at end of file 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 54fe65ddd..0d070d43d 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 @@ -6,7 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; -import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; import java.util.ConcurrentModificationException; import java.util.List; @@ -33,7 +33,7 @@ public interface EntityDescriptorService { * @throws EntityIdExistsException If any EntityDescriptor already exists with the same EntityId */ EntityDescriptorRepresentation createNew(EntityDescriptor ed) - throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException; + throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException; /** * @param edRepresentation Incoming representation to save @@ -42,7 +42,7 @@ EntityDescriptorRepresentation createNew(EntityDescriptor ed) * @throws EntityIdExistsException If the entity already exists */ EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) - throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException; + throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException; /** * Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor @@ -99,10 +99,11 @@ EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepres * @throws ForbiddenException If the user is not permitted to perform the action * @throws EntityNotFoundException If the entity doesn't already exist in the database * @throws ConcurrentModificationException IF the entity is being modified in another session - * @throws InvalidUrlMatchException If the entity id or the ACS location urls don't match the supplied regex + * @throws InvalidPatternMatchException If the entity id or the ACS location urls don't match the supplied regex */ EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) - throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException, InvalidUrlMatchException; + throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException, + InvalidPatternMatchException; /** * Update an instance of entity descriptor with information from the front-end representation 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 f6a979e2d..04b33bf76 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 @@ -5,7 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException; import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException; import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException; -import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException; +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; import edu.internet2.tier.shibboleth.admin.ui.security.model.Group; @@ -73,13 +73,13 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto @Override public EntityDescriptorRepresentation createNew(EntityDescriptor ed) - throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { + throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException { return createNew(createRepresentationFromDescriptor(ed)); } @Override public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) - throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException { + throws ForbiddenException, EntityIdExistsException, InvalidPatternMatchException { if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } @@ -368,7 +368,7 @@ public Map getRelyingPartyOverridesRepresentationFromAttributeLi @Override public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) - throws ForbiddenException, EntityNotFoundException, InvalidUrlMatchException { + throws ForbiddenException, EntityNotFoundException, InvalidPatternMatchException { EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId()); if (existingEd == null) { throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId())); @@ -397,17 +397,17 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata. buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation); } - private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidUrlMatchException { + private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidPatternMatchException { // Check the entity id first if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) { - throw new InvalidUrlMatchException("EntityId is not a pattern match to the group"); + throw new InvalidPatternMatchException("EntityId is not a pattern match to the group"); } // Check the ACS locations if (edRep.getAssertionConsumerServices() != null && edRep.getAssertionConsumerServices().size() > 0) { for (AssertionConsumerServiceRepresentation acs : edRep.getAssertionConsumerServices()) { if (!groupService.doesStringMatchGroupPattern(edRep.getIdOfOwner(), acs.getLocationUrl())) { - throw new InvalidUrlMatchException( + throw new InvalidPatternMatchException( "ACS location [ " + acs.getLocationUrl() + " ] is not a pattern match to the group"); } } 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 717aeb155..5e514763b 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 @@ -11,7 +11,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRe import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException -import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException +import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidPatternMatchException import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.security.model.Group @@ -24,35 +24,18 @@ 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.GroupServiceForTesting import edu.internet2.tier.shibboleth.admin.ui.security.service.GroupServiceImpl -import edu.internet2.tier.shibboleth.admin.ui.security.service.IGroupService import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService -import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorVersionService import edu.internet2.tier.shibboleth.admin.ui.service.EntityService 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.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils -import groovy.json.JsonOutput -import groovy.json.JsonSlurper - -import org.skyscreamer.jsonassert.Customization -import org.skyscreamer.jsonassert.JSONAssert -import org.skyscreamer.jsonassert.JSONCompareMode -import org.skyscreamer.jsonassert.ValueMatcher -import org.skyscreamer.jsonassert.comparator.CustomComparator -import org.skyscreamer.jsonassert.comparator.JSONCompareUtil import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.support.RootBeanDefinition import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Profile -import org.springframework.context.support.StaticApplicationContext import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContext @@ -65,17 +48,10 @@ import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.transaction.annotation.Transactional import org.springframework.web.client.RestTemplate -import org.springframework.web.servlet.config.annotation.EnableWebMvc -import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport -import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver import org.springframework.web.util.NestedServletException -import spock.lang.Ignore -import spock.lang.Shared import spock.lang.Specification import spock.lang.Subject -import java.time.LocalDateTime - import javax.persistence.EntityManager import static org.hamcrest.CoreMatchers.containsString @@ -341,7 +317,7 @@ class EntityDescriptorControllerTests extends Specification { mockMvc.perform(post('/api/EntityDescriptor').contentType(APPLICATION_JSON).content(edRepJson)) false } catch (NestedServletException expected) { - expected.getCause() instanceof InvalidUrlMatchException + expected.getCause() instanceof InvalidPatternMatchException } } From d9414983997e044021c2f7600305eecc1aaf993c Mon Sep 17 00:00:00 2001 From: chasegawa Date: Tue, 17 Aug 2021 21:28:44 -0700 Subject: [PATCH 10/48] SHIBUI-2024 slight refactoring (test cleanup) --- .../repository/GroupsRepositoryTests.groovy | 32 +++--- .../OwnershipRepositoryTests.groovy | 40 +++---- .../security/service/GroupServiceTests.groovy | 4 +- .../security/service/UserServiceTests.groovy | 104 ++++++++---------- 4 files changed, 80 insertions(+), 100 deletions(-) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy index 1a23778da..aba53121e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/GroupsRepositoryTests.groovy @@ -1,23 +1,24 @@ package edu.internet2.tier.shibboleth.admin.ui.security.repository -import javax.persistence.EntityManager -import javax.transaction.Transactional - +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.model.Group +import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership +import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Profile +import org.springframework.dao.DataIntegrityViolationException import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.annotation.Rollback import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration - -import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.security.model.Group -import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership -import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.GroupUpdatedEntityListener import spock.lang.Specification +import javax.transaction.Transactional + /** * Tests to validate the repo and model for groups * @author chasegawa @@ -26,7 +27,7 @@ import spock.lang.Specification @ContextConfiguration(classes=[InternationalizationConfiguration, LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@ActiveProfiles("test") +@ActiveProfiles(["test","gr-tests"]) class GroupsRepositoryTests extends Specification { @Autowired GroupsRepository groupsRepo @@ -95,8 +96,8 @@ class GroupsRepositoryTests extends Specification { it.resourceId = "g1" it } - def Group savedGroup = groupsRepo.saveAndFlush(group) - def Collection all = ownershipRepository.findAllByOwner(savedGroup) + Group savedGroup = groupsRepo.saveAndFlush(group) + Collection all = ownershipRepository.findAllByOwner(savedGroup) then: all.size() == 3 @@ -131,7 +132,7 @@ class GroupsRepositoryTests extends Specification { def gList = groupsRepo.findAll() gList.size() == 1 def groupFromDb = gList.get(0).asType(Group) - groupFromDb.equals(group) == true + groupFromDb.equals(group) // fetch checks groupsRepo.findByResourceId("not an id") == null @@ -158,7 +159,7 @@ class GroupsRepositoryTests extends Specification { then: // Missing non-nullable field (name) should thrown error - final def exception = thrown(org.springframework.dao.DataIntegrityViolationException) + final def exception = thrown(DataIntegrityViolationException) } @Rollback @@ -212,13 +213,14 @@ class GroupsRepositoryTests extends Specification { groupsRepo.findAll().size() == 0 when: - def nothingThere = groupsRepo.findByResourceId(null); + def nothingThere = groupsRepo.findByResourceId(null) then: nothingThere == null } - @org.springframework.boot.test.context.TestConfiguration + @TestConfiguration + @Profile("gr-tests") static class LocalConfig { @Bean GroupUpdatedEntityListener groupUpdatedEntityListener(OwnershipRepository repo) { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy index 229763c58..196dd29f4 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/repository/OwnershipRepositoryTests.groovy @@ -83,7 +83,7 @@ class OwnershipRepositoryTests extends Specification { @Rollback def "test clearUsersGroups"() { when: "remove entries where the user is the owned object of a group" - repo.clearUsersGroups("aaa"); + repo.clearUsersGroups("aaa") def result = repo.findAllGroupsForUser("aaa") then: @@ -102,7 +102,7 @@ class OwnershipRepositoryTests extends Specification { } when: "remove entries where the user is the owned object of groups" - repo.clearUsersGroups("ccc"); + repo.clearUsersGroups("ccc") result = repo.findAllGroupsForUser("ccc") then: @@ -113,9 +113,8 @@ class OwnershipRepositoryTests extends Specification { def "test deleteEntriesForOwnedObject"() { when: "remove entries where the user is the owned object of a group" repo.deleteEntriesForOwnedObject(new Ownable() { - public String getObjectId() { return "aaa" } - - public OwnableType getOwnableType() { OwnableType.USER } + String getObjectId() { return "aaa" } + OwnableType getOwnableType() { OwnableType.USER } }) def result = repo.findAllGroupsForUser("aaa") @@ -136,10 +135,9 @@ class OwnershipRepositoryTests extends Specification { when: "remove entries where the user is the owned object of groups" repo.deleteEntriesForOwnedObject(new Ownable() { - public String getObjectId() { return "ccc" } - - public OwnableType getOwnableType() { OwnableType.USER } - }); + String getObjectId() { return "ccc" } + OwnableType getOwnableType() { OwnableType.USER } + }) result = repo.findAllGroupsForUser("ccc") then: @@ -153,9 +151,8 @@ class OwnershipRepositoryTests extends Specification { userIds.add("bbb") userIds.add("ccc") def result = repo.findUsersByOwner(new Owner() { - public String getOwnerId() { return "g1" } - - public OwnerType getOwnerType() { OwnerType.GROUP } + String getOwnerId() { return "g1" } + OwnerType getOwnerType() { OwnerType.GROUP } }) then: @@ -167,9 +164,8 @@ class OwnershipRepositoryTests extends Specification { when: result = repo.findUsersByOwner(new Owner() { - public String getOwnerId() { return "aaa" } - - public OwnerType getOwnerType() { return OwnerType.USER } + String getOwnerId() { return "aaa" } + OwnerType getOwnerType() { return OwnerType.USER } }) then: @@ -196,8 +192,8 @@ class OwnershipRepositoryTests extends Specification { groupIds.add("g1") groupIds.add("g2") def result = repo.findOwnableObjectOwners(new Ownable() { - public String getObjectId() { return "ccc" } - public OwnableType getOwnableType() { return OwnableType.USER } + String getObjectId() { return "ccc" } + OwnableType getOwnableType() { return OwnableType.USER } }) then: @@ -242,9 +238,8 @@ class OwnershipRepositoryTests extends Specification { userIds.add("bbb") userIds.add("ccc") def result = repo.findAllByOwner(new Owner() { - public String getOwnerId() { return "g1" } - - public OwnerType getOwnerType() { return OwnerType.GROUP } + String getOwnerId() { return "g1" } + OwnerType getOwnerType() { return OwnerType.GROUP } }) then: @@ -256,9 +251,8 @@ class OwnershipRepositoryTests extends Specification { when: "Find all items owned by user aaa" result = repo.findAllByOwner(new Owner() { - public String getOwnerId() { return "aaa" } - - public OwnerType getOwnerType() { return OwnerType.USER } + String getOwnerId() { return "aaa" } + OwnerType getOwnerType() { return OwnerType.USER } }) then: diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy index 7673a907a..eb742ddf2 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy @@ -34,7 +34,7 @@ import spock.lang.Specification @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext -@ActiveProfiles(["local"]) +@ActiveProfiles(["gs-test"]) class GroupServiceTests extends Specification { @Autowired GroupServiceForTesting groupService @@ -126,7 +126,7 @@ class GroupServiceTests extends Specification { } @TestConfiguration - @Profile("local") + @Profile("gs-test") static class LocalConfig { @Bean GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { 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 index 2ef663fbc..732743430 100644 --- 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 @@ -1,61 +1,45 @@ package edu.internet2.tier.shibboleth.admin.ui.security.service -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -import javax.persistence.EntityManager - -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.domain.EntityScan -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.context.TestConfiguration -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.ComponentScan -import org.springframework.context.annotation.Profile -import org.springframework.context.annotation.PropertySource -import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder -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.transaction.annotation.Transactional - import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer - -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.configuration.InternationalizationConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration -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.OwnerType import edu.internet2.tier.shibboleth.admin.ui.security.model.Ownership 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.listener.GroupUpdatedEntityListener -import edu.internet2.tier.shibboleth.admin.ui.security.model.listener.UserUpdatedEntityListener import edu.internet2.tier.shibboleth.admin.ui.security.repository.GroupsRepository import edu.internet2.tier.shibboleth.admin.ui.security.repository.OwnershipRepository 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.IGroupService -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 +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Profile +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder +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.transaction.annotation.Transactional import spock.lang.Specification +import javax.persistence.EntityManager +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + @DataJpaTest @ContextConfiguration(classes=[CoreShibUiConfiguration, CustomPropertiesConfiguration, LocalConfig]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext -@ActiveProfiles(["test", "local"]) +@ActiveProfiles(["test", "us-test"]) @ComponentScan(basePackages="{ edu.internet2.tier.shibboleth.admin.ui.configuration }") class UserServiceTests extends Specification { @@ -106,17 +90,17 @@ class UserServiceTests extends Specification { @Rollback def "When creating user, user is set to the correct group"() { given: - Group gb = new Group(); + 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 user = new User(username: "someUser", roles:[userRole.get()], password: "foo") user.setGroup(gb) when: - def User result = userService.save(user) + User result = userService.save(user) then: result.groupId == "testingGroupBBB" @@ -124,12 +108,12 @@ class UserServiceTests extends Specification { result.userGroups.size() == 1 // Raw check that the DB is correct for ownership - def Set users = ownershipRepository.findUsersByOwner(gb) + Set users = ownershipRepository.findUsersByOwner(gb) users.size() == 1 - users.getAt(0).ownedId == "someUser" + users[0].ownedId == "someUser" // Validate that loading the group has the correct list as well - Group g = groupService.find("testingGroupBBB"); + Group g = groupService.find("testingGroupBBB") g.ownedItems.size() == 1 } @@ -141,19 +125,19 @@ class UserServiceTests extends Specification { ga.setName("Group A") ga = groupService.createGroup(ga) - Group gb = new Group(); + 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 user = new User(username: "someUser", roles:[userRole.get()], password: "foo") user.setGroup(gb) - def User userInB = userService.save(user) + User userInB = userService.save(user) when: userInB.setGroupId("testingGroup") // changing groups will happen by updating the user's groupid (from the ui) - def User result = userService.save(userInB) + User result = userService.save(userInB) then: result.groupId == "testingGroup" @@ -161,19 +145,19 @@ class UserServiceTests extends Specification { result.userGroups.size() == 1 // Raw check that the DB is correct for ownership - def Set users = ownershipRepository.findUsersByOwner(ga) + Set users = ownershipRepository.findUsersByOwner(ga) users.size() == 1 - users.getAt(0).ownedId == "someUser" + users[0].ownedId == "someUser" // check db is correct for the previous group as well - def Set users2 = ownershipRepository.findUsersByOwner(gb) + Set users2 = ownershipRepository.findUsersByOwner(gb) users2.size() == 0 // Validate that loading the group has the correct list as well - Group g = groupService.find("testingGroup"); + Group g = groupService.find("testingGroup") g.ownedItems.size() == 1 - Group g2 = groupService.find("testingGroupBBB"); + Group g2 = groupService.find("testingGroupBBB") g2.ownedItems.size() == 0 } @@ -186,16 +170,16 @@ class UserServiceTests extends Specification { ga = groupService.createGroup(ga) Optional userRole = roleRepository.findByName("ROLE_USER") - def User user = new User(username: "someUser", firstName: "Fred", lastName: "Flintstone", roles:[userRole.get()], password: "foo") + User user = new User(username: "someUser", firstName: "Fred", lastName: "Flintstone", roles:[userRole.get()], password: "foo") user.setGroup(ga) userService.save(user) when: - def User flintstoneUser = userRepository.findByUsername("someUser").get() + User flintstoneUser = userRepository.findByUsername("someUser").get() flintstoneUser.setFirstName("Wilma") flintstoneUser.setGroupId("testingGroup") - def User result = userService.save(flintstoneUser) + User result = userService.save(flintstoneUser) then: result.groupId == "testingGroup" @@ -212,7 +196,7 @@ class UserServiceTests extends Specification { ga.setName("Group A") ga = groupService.createGroup(ga) - Group gb = new Group(); + Group gb = new Group() gb.setResourceId("testingGroupBBB") gb.setName("Group BBB") gb = groupService.createGroup(gb) @@ -237,16 +221,16 @@ class UserServiceTests extends Specification { result.userGroups.size() == 2 // Raw check that the DB is correct for ownership - def Set users = ownershipRepository.findUsersByOwner(ga) + Set users = ownershipRepository.findUsersByOwner(ga) users.size() == 1 - users.getAt(0).ownedId == "someUser" + users[0].ownedId == "someUser" - def Set users2 = ownershipRepository.findUsersByOwner(gb) + Set users2 = ownershipRepository.findUsersByOwner(gb) users2.size() == 1 - users2.getAt(0).ownedId == "someUser" + users2[0].ownedId == "someUser" when: - def userFromDb = userRepository.findById(result.id).get(); + def userFromDb = userRepository.findById(result.id).get() then: userFromDb.getUserGroups().size() == 2 @@ -259,7 +243,7 @@ class UserServiceTests extends Specification { } @TestConfiguration - @Profile("local") + @Profile("us-test") static class LocalConfig { @Bean GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { From 130dd66dc6b50580e7c6a905f1db408cd34fb1d0 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 18 Aug 2021 10:29:55 -0700 Subject: [PATCH 11/48] SHIBUI-2024 slight refactoring (test cleanup) --- .../ui/controller/EntityDescriptorControllerTests.groovy | 6 ++++-- .../admin/ui/security/service/GroupServiceTests.groovy | 2 ++ .../admin/ui/security/service/UserServiceTests.groovy | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) 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 5e514763b..74ce22376 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 @@ -35,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary import org.springframework.context.annotation.Profile import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.security.core.Authentication @@ -64,7 +65,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext -@ActiveProfiles(["local"]) +@ActiveProfiles(["edc-test"]) class EntityDescriptorControllerTests extends Specification { @Autowired EntityDescriptorRepository entityDescriptorRepository @@ -796,9 +797,10 @@ class EntityDescriptorControllerTests extends Specification { } @org.springframework.boot.test.context.TestConfiguration - @Profile(value = "local") + @Profile(value = "edc-test") static class LocalConfig { @Bean + @Primary GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { it.groupRepository = repo diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy index eb742ddf2..a1a15cb56 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/security/service/GroupServiceTests.groovy @@ -18,6 +18,7 @@ import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Primary import org.springframework.context.annotation.Profile import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.test.annotation.DirtiesContext @@ -129,6 +130,7 @@ class GroupServiceTests extends Specification { @Profile("gs-test") static class LocalConfig { @Bean + @Primary GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { it.groupRepository = repo 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 index 732743430..f19049486 100644 --- 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 @@ -20,6 +20,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Primary import org.springframework.context.annotation.Profile import org.springframework.data.jpa.repository.config.EnableJpaRepositories import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder @@ -246,6 +247,7 @@ class UserServiceTests extends Specification { @Profile("us-test") static class LocalConfig { @Bean + @Primary GroupServiceForTesting groupServiceForTesting(GroupsRepository repo, OwnershipRepository ownershipRepository) { GroupServiceForTesting result = new GroupServiceForTesting(new GroupServiceImpl().with { it.groupRepository = repo From feb6e5300f7aeb6c79a18fae9c1080d86d3dcaea Mon Sep 17 00:00:00 2001 From: chasegawa Date: Wed, 18 Aug 2021 12:38:37 -0700 Subject: [PATCH 12/48] SHIBUI-2024 fixing issue with test when there is no owner of an entity id --- ...escriptorControllerVersionEndpointsIntegrationTests.groovy | 2 +- .../admin/ui/service/JPAEntityDescriptorServiceImpl.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy index a8d5a68ab..802002fde 100644 --- a/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy +++ b/backend/src/enversTest/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerVersionEndpointsIntegrationTests.groovy @@ -165,4 +165,4 @@ class EntityDescriptorControllerVersionEndpointsIntegrationTests extends Specifi private static resourceUriFor(String uriTemplate, String resourceId) { String.format(uriTemplate, resourceId) } -} +} \ 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 04b33bf76..86ab2839d 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 @@ -16,6 +16,7 @@ import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -376,6 +377,9 @@ public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRe if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) { throw new ForbiddenException("You do not have the permissions necessary to enable this service."); } + if (StringUtils.isEmpty(edRep.getIdOfOwner())) { + edRep.setIdOfOwner(StringUtils.isNotEmpty(existingEd.getIdOfOwner()) ? existingEd.getIdOfOwner() : userService.getCurrentUserGroup().getOwnerId()); + } if (!userService.isAuthorizedFor(existingEd)) { throw new ForbiddenException(); } From 347bbdf3050e95ced9e9a00a153e8ae254e98c3c Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Wed, 18 Aug 2021 14:25:26 -0700 Subject: [PATCH 13/48] Added field to group form --- .../src/main/resources/i18n/messages.properties | 3 +++ ui/public/assets/schema/groups/group.json | 5 +++++ ui/src/app/admin/component/GroupForm.js | 4 +++- ui/src/app/admin/hooks.js | 16 ++++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 9789f7299..261600110 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -489,6 +489,9 @@ label.by=By label.source=Metadata Source label.provider=Metadata Provider +label.url-validation-regex=URL validation regular expression +tooltip.url-validation-regex=URL validation regular expression + message.user-role-admin-group=Cannot change group for ROLE_ADMIN users. message.delete-user-title=Delete User? diff --git a/ui/public/assets/schema/groups/group.json b/ui/public/assets/schema/groups/group.json index 02bcb2dea..518293851 100644 --- a/ui/public/assets/schema/groups/group.json +++ b/ui/public/assets/schema/groups/group.json @@ -17,6 +17,11 @@ "type": "string", "minLength": 1, "maxLength": 255 + }, + "validationRegex": { + "title": "label.url-validation-regex", + "description": "tooltip.url-validation-regex", + "type": "string" } } } \ No newline at end of file diff --git a/ui/src/app/admin/component/GroupForm.js b/ui/src/app/admin/component/GroupForm.js index da8c5af8b..77ec82135 100644 --- a/ui/src/app/admin/component/GroupForm.js +++ b/ui/src/app/admin/component/GroupForm.js @@ -5,7 +5,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSpinner, faSave } from '@fortawesome/free-solid-svg-icons'; import Translate from '../../i18n/components/translate'; -import { useGroupUiSchema } from '../hooks'; +import { useGroupUiSchema, useGroupUiValidator } from '../hooks'; import { fields, widgets } from '../../form/component'; import { templates } from '../../form/component'; import { FormContext, setFormDataAction, setFormErrorAction } from '../../form/FormManager'; @@ -23,6 +23,7 @@ export function GroupForm ({group = {}, errors = [], loading = false, schema, on }; const uiSchema = useGroupUiSchema(); + const validator = useGroupUiValidator(); return (<>

@@ -49,6 +50,7 @@ export function GroupForm ({group = {}, errors = [], loading = false, schema, on
onChange(form)} + validate={validator} schema={schema} uiSchema={uiSchema} FieldTemplate={templates.FieldTemplate} diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index 28aef201a..483f1edeb 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -1,4 +1,6 @@ import useFetch from 'use-http'; +import isNil from 'lodash/isNil'; +import {isValidRegex} from '../core/utility/is_valid_regex'; import API_BASE_PATH from '../App.constant'; export function useGroups (opts = { cachePolicy: 'no-cache' }) { @@ -17,4 +19,18 @@ export function useGroupUiSchema () { 'ui:widget': 'textarea' } }; +} + +export function useGroupUiValidator() { + console.log('hi') + return (formData, errors) => { + console.log(formData, errors) + if (!isNil(formData?.validationRegex)) { + const isValid = isValidRegex(formData.validationRegex); + if (!isValid) { + errors.validationRegex.addError('message.invalid-regex-pattern'); + } + } + return errors; + } } \ No newline at end of file From 198b1314e044dcc10e02004fa908e68f7a7240a8 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 19 Aug 2021 08:55:56 -0700 Subject: [PATCH 14/48] NOJIRA Including the group detail in the json for users --- .../tier/shibboleth/admin/ui/security/model/User.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 cb769c662..30bc67d14 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 @@ -82,7 +82,6 @@ public class User extends AbstractAuditable implements Owner, Ownable { * @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 */ - @JsonIgnore public Group getGroup() { return getUserGroups().isEmpty() ? null : (Group) userGroups.toArray()[0]; } @@ -145,4 +144,4 @@ public void setGroups(Set groups) { public void registerLoader(ILazyLoaderHelper lazyLoaderHelper) { this.lazyLoaderHelper = lazyLoaderHelper; } -} +} \ No newline at end of file From f9ff3e351353b4ffd8d3c1c00d6d89b223733376 Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 19 Aug 2021 09:30:44 -0700 Subject: [PATCH 15/48] NOJIRA Including the group detail in the json for users --- .../tier/shibboleth/admin/ui/security/model/User.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 30bc67d14..1ab25f4ac 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 @@ -130,7 +130,8 @@ public Set getUserGroups() { } return userGroups; } - + + @JsonIgnore public void setGroup(Group g) { groupId = g.getResourceId(); userGroups.clear(); From 0294ca34286cf3dcba7ead1570ad51bb3015a8eb Mon Sep 17 00:00:00 2001 From: chasegawa Date: Thu, 19 Aug 2021 13:17:49 -0700 Subject: [PATCH 16/48] SHIBUI-2024 Including the entity targets for name and id filter --- .../controller/MetadataFiltersController.java | 9 ++- .../filters/EntityAttributesFilter.java | 73 ++++++++----------- .../filters/EntityAttributesFilterTarget.java | 27 ++++--- .../ui/domain/filters/IFilterTarget.java | 13 ++++ .../admin/ui/domain/filters/ITargetable.java | 5 ++ .../ui/domain/filters/NameIdFormatFilter.java | 26 +++---- .../filters/NameIdFormatFilterTarget.java | 32 ++++---- .../MetadataFiltersControllerTests.groovy | 58 +++++++++++++-- .../admin/ui/util/TestObjectGenerator.groovy | 4 +- 9 files changed, 158 insertions(+), 89 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/IFilterTarget.java create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/ITargetable.java diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java index 7f48faa1a..07d45a6f1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersController.java @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.ITargetable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository; @@ -234,10 +235,10 @@ public ResponseEntity update(@PathVariable String metadataResolverId, * IF the filter is of type "EntityAttributes" AND the target is "ENTITY" THEN check each of the values (which are entityIds) */ private void validateFilterOrThrowHttp400(MetadataFilter createdFilter) { - if ("EntityAttributes".equals(createdFilter.getType())) { - EntityAttributesFilter filter = (EntityAttributesFilter) createdFilter; - if ("ENTITY".equals(filter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType())) { - for (String entityId : filter.getEntityAttributesFilterTarget().getValue()) { + if (createdFilter instanceof ITargetable){ + ITargetable filter = (ITargetable) createdFilter; + if ("ENTITY".equals(filter.getTarget().getTargetTypeValue())) { + for (String entityId : filter.getTarget().getValue()) { if (!groupService.doesStringMatchGroupPattern(userService.getCurrentUser().getGroupId(), entityId)) { throw HTTP_400_BAD_REQUEST_EXCEPTION.get(); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java index 7643f483d..c267d4204 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilter.java @@ -8,44 +8,23 @@ import lombok.ToString; import org.hibernate.envers.Audited; -import javax.persistence.CascadeType; -import javax.persistence.CollectionTable; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.JoinColumn; -import javax.persistence.OneToMany; -import javax.persistence.OneToOne; -import javax.persistence.OrderColumn; -import javax.persistence.PostLoad; -import javax.persistence.Transient; +import javax.persistence.*; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromAttributeReleaseList; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeListFromRelyingPartyOverridesRepresentation; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getAttributeReleaseListFromAttributeList; -import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList; +import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.*; @Entity -@EqualsAndHashCode(callSuper = true, exclude={"attributeRelease", "relyingPartyOverrides"}) +@EqualsAndHashCode(callSuper = true, exclude = { "attributeRelease", "relyingPartyOverrides" }) @Getter @Setter @ToString @Audited -public class EntityAttributesFilter extends MetadataFilter { +public class EntityAttributesFilter extends MetadataFilter implements ITargetable { private static final long serialVersionUID = 1L; - public EntityAttributesFilter() { - type = "EntityAttributes"; - } - - @OneToOne(cascade = CascadeType.ALL) - private EntityAttributesFilterTarget entityAttributesFilterTarget; - @OneToMany(cascade = CascadeType.ALL) @OrderColumn @JsonIgnore @@ -53,25 +32,21 @@ public EntityAttributesFilter() { @Transient private List attributeRelease = new ArrayList<>(); - - public void setAttributeRelease(List attributeRelease) { - this.attributeRelease = attributeRelease; - this.rebuildAttributes(); - } + + @OneToOne(cascade = CascadeType.ALL) + private EntityAttributesFilterTarget entityAttributesFilterTarget; @Transient private Map relyingPartyOverrides; - public void setRelyingPartyOverrides(Map relyingPartyOverridesRepresentation) { - this.relyingPartyOverrides = relyingPartyOverridesRepresentation; - this.rebuildAttributes(); + public EntityAttributesFilter() { + type = "EntityAttributes"; } - //TODO: yeah, I'm not too happy, either - private void rebuildAttributes() { - this.attributes.clear(); - this.attributes.addAll((List) (List)getAttributeListFromAttributeReleaseList(this.attributeRelease)); - this.attributes.addAll((List) (List)getAttributeListFromRelyingPartyOverridesRepresentation(this.relyingPartyOverrides)); + @Override + @JsonIgnore + public IFilterTarget getTarget() { + return entityAttributesFilterTarget; } @PostLoad @@ -82,6 +57,23 @@ public void intoTransientRepresentation() { this.relyingPartyOverrides = getRelyingPartyOverridesRepresentationFromAttributeList(this.attributes); } + //TODO: yeah, I'm not too happy, either + private void rebuildAttributes() { + this.attributes.clear(); + this.attributes.addAll((List) (List) getAttributeListFromAttributeReleaseList(this.attributeRelease)); + this.attributes.addAll((List) (List) getAttributeListFromRelyingPartyOverridesRepresentation(this.relyingPartyOverrides)); + } + + public void setAttributeRelease(List attributeRelease) { + this.attributeRelease = attributeRelease; + this.rebuildAttributes(); + } + + public void setRelyingPartyOverrides(Map relyingPartyOverridesRepresentation) { + this.relyingPartyOverrides = relyingPartyOverridesRepresentation; + this.rebuildAttributes(); + } + private EntityAttributesFilter updateConcreteFilterTypeData(EntityAttributesFilter filterToBeUpdated) { filterToBeUpdated.setEntityAttributesFilterTarget(getEntityAttributesFilterTarget()); filterToBeUpdated.setRelyingPartyOverrides(getRelyingPartyOverrides()); @@ -89,8 +81,7 @@ private EntityAttributesFilter updateConcreteFilterTypeData(EntityAttributesFilt return filterToBeUpdated; } - @Override - public MetadataFilter updateConcreteFilterTypeData(MetadataFilter filterToBeUpdated) { + @Override public MetadataFilter updateConcreteFilterTypeData(MetadataFilter filterToBeUpdated) { return updateConcreteFilterTypeData((EntityAttributesFilter) filterToBeUpdated); } -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java index 0a0da096f..8d9247074 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTarget.java @@ -1,15 +1,13 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; import org.hibernate.envers.AuditOverride; import org.hibernate.envers.Audited; -import javax.persistence.Column; -import javax.persistence.ElementCollection; -import javax.persistence.Entity; -import javax.persistence.OrderColumn; +import javax.persistence.*; import java.util.ArrayList; import java.util.List; @@ -18,10 +16,7 @@ @Audited @AuditOverride(forClass = AbstractAuditable.class) @JsonIgnoreProperties({"handler", "hibernateLazyInitializer"}) -public class EntityAttributesFilterTarget extends AbstractAuditable { - public enum EntityAttributesFilterTargetType { - ENTITY, CONDITION_SCRIPT, CONDITION_REF, REGEX - } +public class EntityAttributesFilterTarget extends AbstractAuditable implements IFilterTarget { private EntityAttributesFilterTargetType entityAttributesFilterTargetType; @@ -34,14 +29,20 @@ public EntityAttributesFilterTargetType getEntityAttributesFilterTargetType() { return entityAttributesFilterTargetType; } - public void setEntityAttributesFilterTargetType(EntityAttributesFilterTargetType entityAttributesFilterTarget) { - this.entityAttributesFilterTargetType = entityAttributesFilterTarget; + @Override + @JsonIgnore + public String getTargetTypeValue() { + return entityAttributesFilterTargetType.name(); } public List getValue() { return value; } + public void setEntityAttributesFilterTargetType(EntityAttributesFilterTargetType entityAttributesFilterTarget) { + this.entityAttributesFilterTargetType = entityAttributesFilterTarget; + } + public void setSingleValue(String value) { List values = new ArrayList<>(); values.add(value); @@ -59,4 +60,8 @@ public String toString() { ", value=" + value + '}'; } -} + + public enum EntityAttributesFilterTargetType { + ENTITY, CONDITION_SCRIPT, CONDITION_REF, REGEX + } +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/IFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/IFilterTarget.java new file mode 100644 index 000000000..d0a2683b1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/IFilterTarget.java @@ -0,0 +1,13 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.filters; + +import java.util.List; + +public interface IFilterTarget { + String getTargetTypeValue(); + + List getValue(); + + void setSingleValue(String value); + + void setValue(List value); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/ITargetable.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/ITargetable.java new file mode 100644 index 000000000..c46ff1c7f --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/ITargetable.java @@ -0,0 +1,5 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.filters; + +public interface ITargetable { + public IFilterTarget getTarget(); +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java index 47bb0810b..efd814375 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilter.java @@ -1,19 +1,14 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; -import java.util.List; - -import javax.persistence.CascadeType; -import javax.persistence.ElementCollection; -import javax.persistence.Entity; -import javax.persistence.OneToOne; -import javax.persistence.OrderColumn; - -import org.hibernate.envers.Audited; - +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.hibernate.envers.Audited; + +import javax.persistence.*; +import java.util.List; @Entity @EqualsAndHashCode(callSuper = true) @@ -21,7 +16,7 @@ @Setter @ToString @Audited -public class NameIdFormatFilter extends MetadataFilter { +public class NameIdFormatFilter extends MetadataFilter implements ITargetable { public NameIdFormatFilter() { type = "NameIDFormat"; @@ -36,6 +31,12 @@ public NameIdFormatFilter() { @OneToOne(cascade = CascadeType.ALL) private NameIdFormatFilterTarget nameIdFormatFilterTarget; + @Override + @JsonIgnore + public IFilterTarget getTarget() { + return nameIdFormatFilterTarget; + } + private NameIdFormatFilter updateConcreteFilterTypeData(NameIdFormatFilter filterToBeUpdated) { filterToBeUpdated.setRemoveExistingFormats(getRemoveExistingFormats()); filterToBeUpdated.setFormats(getFormats()); @@ -47,5 +48,4 @@ private NameIdFormatFilter updateConcreteFilterTypeData(NameIdFormatFilter filte public MetadataFilter updateConcreteFilterTypeData(MetadataFilter filterToBeUpdated) { return updateConcreteFilterTypeData((NameIdFormatFilter) filterToBeUpdated); } - -} +} \ No newline at end of file diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java index eef906500..90445ff75 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/NameIdFormatFilterTarget.java @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.filters; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import lombok.EqualsAndHashCode; @@ -10,6 +11,7 @@ import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.OrderColumn; +import javax.persistence.Transient; import java.util.ArrayList; import java.util.List; @@ -19,30 +21,32 @@ @Audited @AuditOverride(forClass = AbstractAuditable.class) @JsonIgnoreProperties({"handler", "hibernateLazyInitializer"}) -public class NameIdFormatFilterTarget extends AbstractAuditable { - - public enum NameIdFormatFilterTargetType { - ENTITY, CONDITION_SCRIPT, REGEX - } +public class NameIdFormatFilterTarget extends AbstractAuditable implements IFilterTarget { private NameIdFormatFilterTargetType nameIdFormatFilterTargetType; + @ElementCollection + @OrderColumn + private List value; + public NameIdFormatFilterTargetType getNameIdFormatFilterTargetType() { return nameIdFormatFilterTargetType; } - public void setNameIdFormatFilterTargetType(NameIdFormatFilterTargetType nameIdFormatFilterTargetType) { - this.nameIdFormatFilterTargetType = nameIdFormatFilterTargetType; + @Override + @JsonIgnore + public String getTargetTypeValue() { + return nameIdFormatFilterTargetType.name(); } - @ElementCollection - @OrderColumn - private List value; - public List getValue() { return value; } + public void setNameIdFormatFilterTargetType(NameIdFormatFilterTargetType nameIdFormatFilterTargetType) { + this.nameIdFormatFilterTargetType = nameIdFormatFilterTargetType; + } + public void setSingleValue(String value) { List values = new ArrayList<>(); values.add(value); @@ -53,5 +57,7 @@ public void setValue(List value) { this.value = value; } - -} + public enum NameIdFormatFilterTargetType { + ENTITY, CONDITION_SCRIPT, REGEX + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index 280294aa1..d9fb504f8 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -13,6 +13,11 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.repository.FilterRepository import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +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.service.IGroupService +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import edu.internet2.tier.shibboleth.admin.ui.service.FilterService import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator @@ -24,8 +29,10 @@ import org.springframework.beans.factory.annotation.Autowired 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.security.test.context.support.WithMockUser import org.springframework.test.context.ContextConfiguration import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.transaction.annotation.Transactional import org.w3c.dom.Document import spock.lang.Specification import spock.lang.Unroll @@ -53,6 +60,15 @@ class MetadataFiltersControllerTests extends Specification { @Autowired FilterService filterService + @Autowired + IGroupService groupService + + @Autowired + RoleRepository roleRepository + + @Autowired + UserService userService + TestObjectGenerator testObjectGenerator RandomGenerator randomGenerator ObjectMapper mapper @@ -67,7 +83,9 @@ class MetadataFiltersControllerTests extends Specification { static BASE_URI = '/api/MetadataResolvers' + @Transactional def setup() { + groupService.ensureAdminGroupExists() randomGenerator = new RandomGenerator() testObjectGenerator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) mapper = new ObjectMapper() @@ -76,6 +94,8 @@ class MetadataFiltersControllerTests extends Specification { controller = new MetadataFiltersController ( repository: metadataResolverRepository, filterRepository: metadataFilterRepository, + groupService: groupService, + userService: userService, metadataResolverService: new MetadataResolverService() { @Override void reloadFilters(String metadataResolverName) { @@ -94,8 +114,33 @@ class MetadataFiltersControllerTests extends Specification { it } ) - mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + roleRepository.deleteAll() + roleRepository.flush() + if (roleRepository.count() == 0) { + def roles = [new Role().with { + name = 'ROLE_ADMIN' + it + }] + roles.each { + roleRepository.save(it) + } + } + roleRepository.flush() + + def users = [new User().with { + username = 'admin' + password = '{noop}adminpass' + firstName = 'Joe' + lastName = 'Doe' + emailAddress = 'joe@institution.edu' + roles.add(roleRepository.findByName('ROLE_ADMIN').get()) + it + }] + users.each { + it = userService.save(it) + } } def "FilterController.getAll gets all available types of filters"() { @@ -139,6 +184,7 @@ class MetadataFiltersControllerTests extends Specification { } @Unroll + @WithMockUser(value = "admin", roles = ["ADMIN"]) def "FilterController.create creates the desired filter (filterType: #filterType)"(String filterType) { given: def randomFilter = testObjectGenerator.buildRandomFilterOfType(filterType) @@ -161,10 +207,9 @@ class MetadataFiltersControllerTests extends Specification { def postedJsonBody = expectedJsonBody - ~/"id":.*?,/ // remove the "id:," when: - def result = mockMvc.perform( - post("$BASE_URI/foo/Filters") - .contentType(APPLICATION_JSON) - .content(postedJsonBody)) + def result = mockMvc.perform(post("$BASE_URI/foo/Filters") + .contentType(APPLICATION_JSON) + .content(postedJsonBody)) then: println postedJsonBody @@ -182,6 +227,7 @@ class MetadataFiltersControllerTests extends Specification { } @Unroll + @WithMockUser(value = "admin", roles = ["ADMIN"]) def "FilterController.update updates the target #filterType filter as desired"(String filterType) { given: def originalFilter = testObjectGenerator.buildRandomFilterOfType(filterType) @@ -251,4 +297,4 @@ class MetadataFiltersControllerTests extends Specification { then: result.andExpect(status().is(409)) } -} +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index e28220cc6..677b159f9 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -275,6 +275,7 @@ class TestObjectGenerator { it.setEntityAttributesFilterTarget(entityAttributesFilter.entityAttributesFilterTarget) it.setAttributes(entityAttributesFilter.attributes) it.intoTransientRepresentation() + it.setEntityAttributesFilterTarget(entityAttributesFilter.getEntityAttributesFilterTarget()) it } } @@ -285,6 +286,7 @@ class TestObjectGenerator { it.resourceId = nameIdFormatFilter.resourceId it.removeExistingFormats = nameIdFormatFilter.removeExistingFormats it.formats = nameIdFormatFilter.formats + it.setNameIdFormatFilterTarget(nameIdFormatFilter.getNameIdFormatFilterTarget()) it } } @@ -659,4 +661,4 @@ class TestObjectGenerator { } return list } -} +} \ No newline at end of file From 8cacc8830b4f683aa1049e7aef73c5dc1068145b Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Fri, 20 Aug 2021 12:01:19 -0700 Subject: [PATCH 17/48] Implemented regex validation --- .../main/resources/i18n/messages.properties | 2 + ui/src/app/admin/hooks.js | 2 - ui/src/app/admin/hooks.test.js | 9 +++++ ui/src/app/core/user/UserContext.js | 37 +++++++++++++++++-- .../component/fields/FilterTargetField.js | 35 +++++++++++++----- .../filter/definition/BaseFilterDefinition.js | 32 ++++++++++++++++ .../EntityAttributesFilterDefinition.js | 29 +-------------- .../definition/NameIdFilterDefinition.js | 30 +-------------- .../definition/BaseProviderDefinition.js | 2 +- .../DynamicHttpMetadataProviderDefinition.js | 13 ++++++- ...ileBackedHttpMetadataProviderDefinition.js | 16 ++++++++ .../source/definition/SourceDefinition.js | 16 +++++++- ui/src/app/metadata/editor/MetadataEditor.js | 4 +- .../app/metadata/editor/MetadataEditorForm.js | 10 ++++- .../metadata/editor/MetadataFilterEditor.js | 4 +- .../metadata/wizard/MetadataProviderWizard.js | 4 +- .../metadata/wizard/MetadataSourceWizard.js | 4 +- 17 files changed, 170 insertions(+), 79 deletions(-) create mode 100644 ui/src/app/admin/hooks.test.js diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties index 261600110..a5cab3353 100644 --- a/backend/src/main/resources/i18n/messages.properties +++ b/backend/src/main/resources/i18n/messages.properties @@ -503,6 +503,8 @@ message.delete-group-body=You are requesting to delete a group. If you complete message.delete-attribute-title=Delete Attribute? message.delete-attribute-body=You are requesting to delete a custom attribute. If you complete this process the attribute will be removed. This cannot be undone. Do you wish to continue? +message.group-pattern-fail=Pattern must match group url validation pattern. + message.must-be-unique=Must be unique. message.must-be-number=Must be a number. message.name-must-be-unique=Name must be unique. diff --git a/ui/src/app/admin/hooks.js b/ui/src/app/admin/hooks.js index 483f1edeb..f0a768cdc 100644 --- a/ui/src/app/admin/hooks.js +++ b/ui/src/app/admin/hooks.js @@ -22,9 +22,7 @@ export function useGroupUiSchema () { } export function useGroupUiValidator() { - console.log('hi') return (formData, errors) => { - console.log(formData, errors) if (!isNil(formData?.validationRegex)) { const isValid = isValidRegex(formData.validationRegex); if (!isValid) { diff --git a/ui/src/app/admin/hooks.test.js b/ui/src/app/admin/hooks.test.js new file mode 100644 index 000000000..303540317 --- /dev/null +++ b/ui/src/app/admin/hooks.test.js @@ -0,0 +1,9 @@ +import { useGroupUiValidator } from './hooks'; + +it('should validate against a regex', () => { + const validator = useGroupUiValidator(); + const addErrorSpy = jest.fn(); + const fail = validator({ validationRegex: '))(()' }, { validationRegex: { addError: addErrorSpy } }); + expect(addErrorSpy).toHaveBeenCalled(); + expect(validator({validationRegex: '/*'})).toBeUndefined(); +}); \ No newline at end of file diff --git a/ui/src/app/core/user/UserContext.js b/ui/src/app/core/user/UserContext.js index d656ac4a4..064c1e65f 100644 --- a/ui/src/app/core/user/UserContext.js +++ b/ui/src/app/core/user/UserContext.js @@ -7,23 +7,33 @@ const UserContext = React.createContext(); const { Provider, Consumer } = UserContext; const path = '/admin/users/current'; +const group = { + "name": "ADMIN-GROUP", + "resourceId": "admingroup", + "validationRegex": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$", + "ownerType": "GROUP", + "ownerId": "admingroup" +}; /*eslint-disable react-hooks/exhaustive-deps*/ function UserProvider({ children }) { const { get, response } = useFetch(`${API_BASE_PATH}`, { - cacheLife: 10000, - cachePolicy: 'cache-first' + cachePolicy: 'no-cache' }); React.useEffect(() => { loadUser() }, []); async function loadUser() { const user = await get(`${path}`); - if (response.ok) setUser(user); + if (response.ok) setUser({ + ...user, + group + }); } const [user, setUser] = React.useState({}); + return ( {children} ); @@ -50,5 +60,24 @@ function useIsAdminOrInGroup() { return isAdmin || isInGroup; } +function useUserGroup() { + const user = useCurrentUser(); + return user?.group; +} + +function useUserGroupRegexValidator () { + const user = useCurrentUser(); + return user?.group?.validationRegex; +} + -export { UserContext, UserProvider, Consumer as UserConsumer, useCurrentUser, useIsAdmin, useIsAdminOrInGroup }; \ No newline at end of file +export { + UserContext, + UserProvider, + Consumer as UserConsumer, + useCurrentUser, + useIsAdmin, + useIsAdminOrInGroup, + useUserGroupRegexValidator, + useUserGroup +}; \ No newline at end of file diff --git a/ui/src/app/form/component/fields/FilterTargetField.js b/ui/src/app/form/component/fields/FilterTargetField.js index 959306b71..9aaf84b97 100644 --- a/ui/src/app/form/component/fields/FilterTargetField.js +++ b/ui/src/app/form/component/fields/FilterTargetField.js @@ -45,8 +45,13 @@ const FilterTargetField = ({ onChange, errorSchema, formData, + formContext, ...props }) => { + + const { group } = formContext; + const regex = new RegExp(group?.validationRegex || '/*'); + const typeFieldName = `${name}Type`; const type = schema.properties[typeFieldName]; @@ -59,6 +64,8 @@ const FilterTargetField = ({ const [selectedTarget, setSelectedTarget] = React.useState([...(formData.value && !isNil(formData.value) && !isNil(formData.value[0]) ? formData.value : [])]); const [term, setSearchTerm] = React.useState(''); + const [match, setMatch] = React.useState(true); + const [touched, setTouched] = React.useState(false); const [ids, setSearchIds] = React.useState([]); const { get, response } = useFetch(`${API_BASE_PATH}/EntityIds/search`, { @@ -113,6 +120,7 @@ const FilterTargetField = ({ const onEntityIdsChange = (value) => { setSearchTerm(value); + setMatch(regex ? regex.test(value) : true); }; const selectType = (option) => { @@ -172,19 +180,28 @@ const FilterTargetField = ({ onInputChange={onEntityIdsChange} selected={ [term] } onChange={ () => {} } + onBlur={() => setTouched(true)} onSearch={ (query) => setSearchTerm(query) } - renderMenuItemChildren={(option, { options, text }, index) => { - return {option}; - }}> + renderMenuItemChildren={(option, { options, text }, index) => + {option} + }> {({ isMenuShown, toggleMenu }) => ( toggleMenu()} disabled={disabled || readonly} /> )} - - - You must add at least one entity id target and they must each be unique. - - + {(!touched || match) ? + + + You must add at least one entity id target and they must each be unique. + + + : + + + Invalid URL + + + } } { targetType === 'CONDITION_SCRIPT' && @@ -232,7 +249,7 @@ const FilterTargetField = ({