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 64f33a01d..f094ff2f9 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 @@ -5,6 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.* import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import groovy.util.logging.Slf4j @@ -22,6 +23,9 @@ import org.opensaml.saml.saml2.metadata.EntityDescriptor import org.springframework.beans.factory.annotation.Autowired import org.w3c.dom.Document +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.CLASSPATH +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.SVN + @Slf4j class JPAMetadataResolverServiceImpl implements MetadataResolverService { @@ -272,4 +276,49 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { childNodes() } } + + void constructXmlNodeForResolver(ResourceBackedMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { + //This might throw an InvalidResourceTypeException if both resource types do not satisfy validation rules + //But this validation step already would have been performed by higher app layers such as REST controllers, + //and if this is not done, an exception thrown here would be trully considered a server side error bug which would + //need to be taken care of + def resourceType = resolver.validateAndDetermineResourceType() + + markupBuilderDelegate.MetadataProvider( + id: resolver.name, + 'xsi:type': 'ResourceBackedMetadataProvider', + parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, + minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, + maxRefreshDelay: resolver.reloadableMetadataResolverAttributes?.maxRefreshDelay, + refreshDelayFactor: resolver.reloadableMetadataResolverAttributes?.refreshDelayFactor, + indexesRef: resolver.reloadableMetadataResolverAttributes?.indexesRef, + resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, + expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { + + if(resourceType == SVN) { + MetadataResource( + 'xmlns:resource': 'urn:mace:shibboleth:2.0:resource', + 'xsi:type': 'resource:SVNResource', + 'resourceFile': resolver.svnMetadataResource.resourceFile, + 'repositoryURL': resolver.svnMetadataResource.repositoryURL, + 'workingCopyDirectory': resolver.svnMetadataResource.workingCopyDirectory, + 'username': resolver.svnMetadataResource.username, + 'password': resolver.svnMetadataResource.password, + 'proxyHost': resolver.svnMetadataResource.proxyHost, + 'proxyPort': resolver.svnMetadataResource.proxyHost, + 'proxyUserName': resolver.svnMetadataResource.proxyUserName, + 'proxyPassword': resolver.svnMetadataResource.proxyPassword) + + } + else if (resourceType == CLASSPATH) { + MetadataResource( + 'xmlns:resource': 'urn:mace:shibboleth:2.0:resource', + 'xsi:type': 'resource:ClasspathResource', + 'file': resolver.classpathMetadataResource.file) + } + + childNodes() + } + + } } 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 new file mode 100644 index 000000000..6957f71b1 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverValidationConfiguration.java @@ -0,0 +1,24 @@ +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 org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class MetadataResolverValidationConfiguration { + + @Bean + ResourceBackedMetadataResolverValidator resourceBackedMetadataResolverValidator() { + return new ResourceBackedMetadataResolverValidator(); + } + + @Bean + @SuppressWarnings("Unchecked") + MetadataResolverValidationService metadataResolverValidationService(List metadataResolverValidators) { + return new MetadataResolverValidationService(metadataResolverValidators); + } +} 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 1902c212b..7d873afa6 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 @@ -29,7 +29,7 @@ import java.util.stream.Stream; @RestController -@RequestMapping("/api/MetadataResolver/{metadataResolverId}") +@RequestMapping("/api/MetadataResolvers/{metadataResolverId}") public class MetadataFiltersController { private static Logger LOGGER = LoggerFactory.getLogger(MetadataFiltersController.class); @@ -194,7 +194,7 @@ else if(filterWithUpdatedData instanceof RequiredValidUntilFilter) { private static URI getResourceUriFor(MetadataResolver mr, String filterResourceId) { return ServletUriComponentsBuilder - .fromCurrentServletMapping().path("/api/MetadataResolver/") + .fromCurrentServletMapping().path("/api/MetadataResolvers/") .pathSegment(mr.getResourceId()) .pathSegment("Filters") .pathSegment(filterResourceId) 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 new file mode 100644 index 000000000..ae75b3f6d --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -0,0 +1,120 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +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.MetadataResolverValidator; +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; + +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; + +@RestController +@RequestMapping("/api") +@Slf4j +public class MetadataResolversController { + + @Autowired + MetadataResolverRepository resolverRepository; + + @Autowired + MetadataResolverValidationService metadataResolverValidationService; + + @GetMapping("/MetadataResolvers") + @Transactional(readOnly = true) + public ResponseEntity getAll() { + Iterable resolvers = resolverRepository.findAll(); + resolvers.forEach(MetadataResolver::updateVersion); + return ResponseEntity.ok(resolvers); + } + + @GetMapping("/MetadataResolvers/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOne(@PathVariable String resourceId) { + MetadataResolver resolver = resolverRepository.findByResourceId(resourceId); + if (resolver == null) { + return ResponseEntity.notFound().build(); + } + resolver.updateVersion(); + return ResponseEntity.ok(resolver); + } + + @PostMapping("/MetadataResolvers") + @Transactional + public ResponseEntity create(@RequestBody MetadataResolver newResolver) { + //TODO: we are disregarding attached filters if any sent from UI. + //Only deal with filters via filters endpoints? + newResolver.clearAllFilters(); + + ResponseEntity validationErrorResponse = validate(newResolver); + if(validationErrorResponse != null) { + return validationErrorResponse; + } + + MetadataResolver persistedResolver = resolverRepository.save(newResolver); + persistedResolver.updateVersion(); + + return ResponseEntity.created(getResourceUriFor(persistedResolver)).body(persistedResolver); + } + + @PutMapping("/MetadataResolvers/{resourceId}") + @Transactional + public ResponseEntity update(@PathVariable String resourceId, @RequestBody MetadataResolver updatedResolver) { + MetadataResolver existingResolver = resolverRepository.findByResourceId(resourceId); + if (existingResolver == null) { + return ResponseEntity.notFound().build(); + } + if (existingResolver.hashCode() != updatedResolver.getVersion()) { + log.info("Metadata Resolver version conflict. Latest resolver in database version: {}. Resolver version sent from UI: {}", + existingResolver.hashCode(), updatedResolver.getVersion()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + + ResponseEntity validationErrorResponse = validate(updatedResolver); + if(validationErrorResponse != null) { + return validationErrorResponse; + } + + updatedResolver.setAudId(existingResolver.getAudId()); + + //TODO: we are disregarding attached filters if any sent from UI. + //Only deal with filters via filters endpoints? + updatedResolver.setMetadataFilters(existingResolver.getMetadataFilters()); + + MetadataResolver persistedResolver = resolverRepository.save(updatedResolver); + persistedResolver.updateVersion(); + + return ResponseEntity.ok(persistedResolver); + } + + @SuppressWarnings("Unchecked") + private ResponseEntity validate(MetadataResolver metadataResolver) { + ValidationResult validationResult = metadataResolverValidationService.validateIfNecessary(metadataResolver); + if(!validationResult.isValid()) { + return ResponseEntity.badRequest().body(validationResult.getErrorMessage()); + } + return null; + } + + private static URI getResourceUriFor(MetadataResolver resolver) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/MetadataResolvers/") + .pathSegment(resolver.getResourceId()) + .build() + .toUri(); + } +} 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 7b267d1ee..0208f92c4 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 @@ -55,7 +55,7 @@ public EntityAttributesFilter() { public void intoTransientRepresentation() { this.attributeRelease = getAttributeReleaseListFromAttributeList(this.attributes); this.relyingPartyOverrides = getRelyingPartyOverridesRepresentationFromAttributeList(attributes); - setVersion(hashCode()); + updateVersion(); } @PrePersist diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java index ebfa1b5a6..0347de1f7 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/filters/MetadataFilter.java @@ -47,4 +47,8 @@ public class MetadataFilter extends AbstractAuditable { @Transient private int version; + + public void updateVersion() { + this.version = hashCode(); + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ClasspathMetadataResource.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ClasspathMetadataResource.java new file mode 100644 index 000000000..a3dc076ab --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ClasspathMetadataResource.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Embeddable; + +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@EqualsAndHashCode +public class ClasspathMetadataResource { + + private String file; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java index 0910c9050..e1c9ddc6b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicHttpMetadataResolver.java @@ -21,6 +21,8 @@ @ToString public class DynamicHttpMetadataResolver extends MetadataResolver { + + public static final String DEFAULT_TIMEOUT = "PT5S"; @Embedded @@ -38,6 +40,7 @@ public class DynamicHttpMetadataResolver extends MetadataResolver { private List supportedContentTypes; public DynamicHttpMetadataResolver() { + type = "DynamicHttpMetadataResolver"; this.httpMetadataResolverAttributes = new HttpMetadataResolverAttributes(); this.httpMetadataResolverAttributes.setConnectionRequestTimeout(DEFAULT_TIMEOUT); this.httpMetadataResolverAttributes.setConnectionTimeout(DEFAULT_TIMEOUT); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java index 37748e6c0..f659a412b 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java @@ -11,12 +11,15 @@ @Entity @EqualsAndHashCode(callSuper = true) -@NoArgsConstructor @Getter @Setter @ToString public class FileBackedHttpMetadataResolver extends MetadataResolver { + public FileBackedHttpMetadataResolver() { + type = "FileBackedHttpMetadataResolver"; + } + private String metadataURL; private String backingFile; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java index 63ec78eb5..2bdb67d1f 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/LocalDynamicMetadataResolver.java @@ -2,7 +2,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @@ -11,12 +10,15 @@ @Entity @EqualsAndHashCode(callSuper = true) -@NoArgsConstructor @Getter @Setter @ToString public class LocalDynamicMetadataResolver extends MetadataResolver { + public LocalDynamicMetadataResolver() { + type = "LocalDynamicMetadataResolver"; + } + private String sourceDirectory; private String sourceManagerRef; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java index 1bbafefba..adbb30f0d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -1,5 +1,8 @@ package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import lombok.EqualsAndHashCode; @@ -22,17 +25,25 @@ @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) -@EqualsAndHashCode(callSuper = true, exclude={"version"}) +@EqualsAndHashCode(callSuper = true, exclude = {"version"}) @NoArgsConstructor @Getter @Setter @ToString +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "@type", visible = true) +@JsonSubTypes({@JsonSubTypes.Type(value = LocalDynamicMetadataResolver.class, name = "LocalDynamicMetadataResolver"), + @JsonSubTypes.Type(value = FileBackedHttpMetadataResolver.class, name = "FileBackedHttpMetadataResolver"), + @JsonSubTypes.Type(value = DynamicHttpMetadataResolver.class, name = "DynamicHttpMetadataResolver")}) public class MetadataResolver extends AbstractAuditable { - @Column(unique=true) + @JsonProperty("@type") + @Transient + String type = "BaseMetadataResolver"; + + @Column(unique = true) private String name; - @Column(unique=true) + @Column(unique = true) private String resourceId = UUID.randomUUID().toString(); private Boolean requireValidMetadata = true; @@ -53,4 +64,12 @@ public class MetadataResolver extends AbstractAuditable { @Transient private int version; + + public void updateVersion() { + this.version = hashCode(); + } + + public void clearAllFilters() { + this.metadataFilters.clear(); + } } 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/MetadataResolverValidationService.java new file mode 100644 index 000000000..26039b81b --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationService.java @@ -0,0 +1,40 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 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, conciders provided MetadataResolver as valid. + *

+ * Uses chain-of-responsibility design pattern + * + * @author Dmitriy Kopylenko + */ +public class MetadataResolverValidationService { + + private List> validators; + + public MetadataResolverValidationService(List> validators) { + this.validators = validators != null ? validators : new ArrayList<>(); + } + + @SuppressWarnings("Uncheked") + public ValidationResult validateIfNecessary(T metadataResolver) { + Optional> validator = + this.validators + .stream() + .filter(v -> v.supports(metadataResolver)) + .findFirst(); + return validator.isPresent() ? validator.get().validate(metadataResolver) : new ValidationResult(null); + + } + + //Package-private - used for unit tests + boolean noValidatorsConfigured() { + return this.validators.size() == 0; + } +} 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/MetadataResolverValidator.java new file mode 100644 index 000000000..e6afc7782 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidator.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +/** + * An SPI to validate different types of {@link MetadataResolver}s. + *

+ * Typical usage is - multiple validators for concrete type of resolvers are configured in Spring Application Context, + * aggregated by {@link MetadataResolverValidationService} facade and then that facade is injected into upstream consumers of it + * such as REST controllers, etc. + * + * @author Dmitriy Kopylenko + */ +public interface MetadataResolverValidator { + + boolean supports(MetadataResolver resolver); + + ValidationResult validate(T resolver); + + class ValidationResult { + + public ValidationResult(String errorMessage) { + this.errorMessage = errorMessage; + } + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public boolean isValid() { + return this.errorMessage == null; + } + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolver.java new file mode 100644 index 000000000..2133d88eb --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolver.java @@ -0,0 +1,50 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.Embedded; +import javax.persistence.Entity; + +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.CLASSPATH; +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver.ResourceType.SVN; + +@Entity +@EqualsAndHashCode(callSuper = true) +@Getter +@Setter +@ToString +public class ResourceBackedMetadataResolver extends MetadataResolver { + + @Embedded + private ReloadableMetadataResolverAttributes reloadableMetadataResolverAttributes; + + @Embedded + private ClasspathMetadataResource classpathMetadataResource; + + @Embedded + private SvnMetadataResource svnMetadataResource; + + public ResourceType validateAndDetermineResourceType() throws InvalidResourceTypeException { + if (classpathMetadataResource == null && svnMetadataResource == null) { + throw new InvalidResourceTypeException("No metadata resource is provided. Exactly one is required"); + } + if (classpathMetadataResource != null && svnMetadataResource != null) { + throw new InvalidResourceTypeException("Too many metadata resources are provided. Exactly one is required"); + } + return classpathMetadataResource != null ? CLASSPATH : SVN; + } + + public class InvalidResourceTypeException extends IllegalStateException { + public InvalidResourceTypeException(String s) { + super(s); + } + } + + public enum ResourceType { + CLASSPATH, + SVN + } +} 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/ResourceBackedMetadataResolverValidator.java new file mode 100644 index 000000000..0a828fbed --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataResolverValidator.java @@ -0,0 +1,20 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +public class ResourceBackedMetadataResolverValidator implements MetadataResolverValidator { + + @Override + public boolean supports(MetadataResolver resolver) { + return resolver instanceof ResourceBackedMetadataResolver; + } + + @Override + public ValidationResult validate(ResourceBackedMetadataResolver resolver) { + try { + resolver.validateAndDetermineResourceType(); + } + catch (ResourceBackedMetadataResolver.InvalidResourceTypeException e) { + return new ValidationResult(e.getMessage()); + } + return new ValidationResult(null); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/SvnMetadataResource.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/SvnMetadataResource.java new file mode 100644 index 000000000..06f039bc6 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/SvnMetadataResource.java @@ -0,0 +1,34 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Embeddable; + +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@EqualsAndHashCode +public class SvnMetadataResource { + + private String repositoryURL; + + private String workingCopyDirectory; + + private String resourceFile; + + private String username; + + private String password; + + private String proxyHost; + + private String proxyUserName; + + private String proxyPassword; +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index ac9a01597..733962eb0 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -5,6 +5,8 @@ #logging.config=classpath:log4j2.xml #logging.level.org.springframework.web=ERROR +logging.level.edu.internet2.tier.shibboleth.admin.ui=INFO + # Database Credentials spring.datasource.username=shibui spring.datasource.password=shibui 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 new file mode 100644 index 000000000..d5c2f4f29 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/TestMetadataResolverValidationConfiguration.groovy @@ -0,0 +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 org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + + +@Configuration +class TestMetadataResolverValidationConfiguration { + + @Bean + ResourceBackedMetadataResolverValidator resourceBackedMetadataResolverValidator() { + new ResourceBackedMetadataResolverValidator() + } + + @Bean + MetadataResolverValidationService metadataResolverValidationServiceEmpty() { + new MetadataResolverValidationService(null) + } + + @Bean + MetadataResolverValidationService metadataResolverValidationServiceOneValidator(List metadataResolverValidators) { + new MetadataResolverValidationService(metadataResolverValidators) + } + +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy index c70e6f44f..946413ab5 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy @@ -1,17 +1,11 @@ package edu.internet2.tier.shibboleth.admin.ui.controller -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects -import net.shibboleth.ext.spring.resource.ResourceHelper -import org.joda.time.DateTime -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver -import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain -import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver +import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean -import org.springframework.core.io.ClassPathResource import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.web.util.DefaultUriBuilderFactory @@ -46,33 +40,9 @@ class DefaultAuthenticationIntegrationTests extends Specification { @TestConfiguration static class Config { - @Autowired - OpenSamlObjects openSamlObjects - @Bean MetadataResolver metadataResolver() { - def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) - def aggregate = new ResourceBackedMetadataResolver(resource){ - @Override - DateTime getLastRefresh() { - return null - } - } - - aggregate.with { - it.metadataFilter = new MetadataFilterChain() - it.id = 'testme' - it.parserPool = openSamlObjects.parserPool - it.initialize() - it - } - - return new ChainingMetadataResolver().with { - it.id = 'chain' - it.resolvers = [aggregate] - it.initialize() - it - } + new FilesystemMetadataResolver(new File('fake')) } } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderControllerTests.groovy index bf4ce2ffe..5b10c9f6e 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DynamicHttpMetadataProviderControllerTests.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration 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.repository.DynamicHttpMetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -27,7 +28,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class DynamicHttpMetadataProviderControllerTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy index 2ed405784..33a407f1a 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy @@ -6,6 +6,7 @@ import org.joda.time.DateTime import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain +import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -147,6 +148,7 @@ class EntitiesControllerIntegrationTests extends Specification { !diff.hasDifferences() } + @TestConfiguration static class Config { @Autowired diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/LocalDynamicMetadataProviderControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/LocalDynamicMetadataProviderControllerTests.groovy index bc13df607..14c925c5c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/LocalDynamicMetadataProviderControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/LocalDynamicMetadataProviderControllerTests.groovy @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration 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.repository.LocalDynamicMetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -27,7 +28,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class LocalDynamicMetadataProviderControllerTests extends Specification { 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 aeebeb8a0..d160411db 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 @@ -21,7 +21,6 @@ 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.test.context.ContextConfiguration -import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.w3c.dom.Document import spock.lang.Specification @@ -57,7 +56,7 @@ class MetadataFiltersControllerTests extends Specification { def mockMvc - def mockFilterService = Mock(FilterService) + static BASE_URI = '/api/MetadataResolvers' def setup() { randomGenerator = new RandomGenerator() @@ -94,7 +93,7 @@ class MetadataFiltersControllerTests extends Specification { def expectedResponseContentType = APPLICATION_JSON_UTF8 when: - def result = mockMvc.perform(get('/api/MetadataResolver/foo/Filters')) + def result = mockMvc.perform(get("$BASE_URI/foo/Filters")) then: result.andExpect(expectedHttpResponseStatus) @@ -114,7 +113,7 @@ class MetadataFiltersControllerTests extends Specification { def expectedResponseContentType = APPLICATION_JSON_UTF8 when: - def result = mockMvc.perform(get("/api/MetadataResolver/foo/Filters/$expectedResourceId")) + def result = mockMvc.perform(get("$BASE_URI/foo/Filters/$expectedResourceId")) then: result.andExpect(expectedHttpResponseStatus) @@ -140,13 +139,13 @@ class MetadataFiltersControllerTests extends Specification { def expectedMetadataResolverUUID = metadataResolver.getResourceId() def expectedFilterUUID = randomFilter.getResourceId() def expectedResponseHeader = 'Location' - def expectedResponseHeaderValue = "/api/MetadataResolver/$expectedMetadataResolverUUID/Filters/$expectedFilterUUID" + def expectedResponseHeaderValue = "$BASE_URI/$expectedMetadataResolverUUID/Filters/$expectedFilterUUID" def expectedJsonBody = mapper.writeValueAsString(randomFilter) def postedJsonBody = expectedJsonBody - ~/"id":.*?,/ // remove the "id:," when: def result = mockMvc.perform( - post('/api/MetadataResolver/foo/Filters') + post("$BASE_URI/foo/Filters") .contentType(APPLICATION_JSON_UTF8) .content(postedJsonBody)) @@ -189,7 +188,7 @@ class MetadataFiltersControllerTests extends Specification { when: def result = mockMvc.perform( - put("/api/MetadataResolver/foo/Filters/$filterUUID") + put("$BASE_URI/foo/Filters/$filterUUID") .contentType(APPLICATION_JSON_UTF8) .content(postedJsonBody)) @@ -228,7 +227,7 @@ class MetadataFiltersControllerTests extends Specification { when: def result = mockMvc.perform( - put("/api/MetadataResolver/foo/Filters/$filterUUID") + put("$BASE_URI/foo/Filters/$filterUUID") .contentType(APPLICATION_JSON_UTF8) .content(postedJsonBody)) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy new file mode 100644 index 000000000..03ae982a5 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -0,0 +1,214 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.opensaml.saml.metadata.resolver.MetadataResolver +import org.opensaml.saml.metadata.resolver.impl.FilesystemMetadataResolver +import org.springframework.beans.factory.annotation.Autowired +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.http.HttpEntity +import org.springframework.http.HttpHeaders + +import org.springframework.test.context.ActiveProfiles + +import spock.lang.Specification + +import static org.springframework.http.HttpMethod.PUT + +/** + * @author Dmitriy Kopylenko + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("no-auth") +class MetadataResolversControllerIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + @Autowired + MetadataResolverRepository metadataResolverRepository + + JsonSlurper jsonSlurper = new JsonSlurper() + + static BASE_URI = '/api/MetadataResolvers' + + def cleanup() { + metadataResolverRepository.deleteAll() + } + + def "GET empty -> /api/MetadataResolvers"() { + when: 'No resolvers are available in data store' + def result = this.restTemplate.getForEntity(BASE_URI, String) + def returnedResolvers = jsonSlurper.parseText(result.body) + + then: + result.statusCodeValue == 200 + returnedResolvers.size() == 0 + } + + def "GET one available MetadataResolver -> /api/MetadataResolvers"() { + given: 'One resolver is available in data store' + def resolver = new DynamicHttpMetadataResolver().with { + it.name = 'Test DynamicHttpMetadataResolver' + it + } + metadataResolverRepository.save(resolver) + + when: 'GET request is made' + def result = this.restTemplate.getForEntity(BASE_URI, String) + def returnedResolvers = jsonSlurper.parseText(result.body) + + then: + result.statusCodeValue == 200 + returnedResolvers.size() == 1 + returnedResolvers[0]['@type'] == 'DynamicHttpMetadataResolver' + returnedResolvers[0].name == 'Test DynamicHttpMetadataResolver' + + } + + def "GET multiple available MetadataResolvers -> /api/MetadataResolvers"() { + given: 'Two resolvers are available in data store' + def resolvers = [ + new DynamicHttpMetadataResolver().with { + it.name = 'Test DynamicHttpMetadataResolver' + it + }, + new FileBackedHttpMetadataResolver().with { + it.name = 'Test FileBackedHttpMetadataResolver' + it + } + ] + resolvers.each { + metadataResolverRepository.save(it) + } + + when: 'GET request is made' + def result = this.restTemplate.getForEntity(BASE_URI, String) + def returnedResolvers = jsonSlurper.parseText(result.body) + + then: + result.statusCodeValue == 200 + returnedResolvers.size() == 2 + returnedResolvers[0]['@type'] == 'DynamicHttpMetadataResolver' + returnedResolvers[0].name == 'Test DynamicHttpMetadataResolver' + returnedResolvers[1]['@type'] == 'FileBackedHttpMetadataResolver' + returnedResolvers[1].name == 'Test FileBackedHttpMetadataResolver' + + } + + def "GET concrete MetadataResolver -> /api/MetadataResolvers/{resourceId}"() { + given: 'One resolver is available in data store' + def resolver = new DynamicHttpMetadataResolver().with { + it.name = 'Test DynamicHttpMetadataResolver' + it + } + def resolverResourceId = resolver.resourceId + metadataResolverRepository.save(resolver) + + when: 'GET request is made with resource Id matching the existing resolver' + def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) + def returnedResolver = jsonSlurper.parseText(result.body) + + then: + result.statusCodeValue == 200 + returnedResolver['@type'] == 'DynamicHttpMetadataResolver' + returnedResolver.name == 'Test DynamicHttpMetadataResolver' + + } + + def "GET non-existent MetadataResolver -> /api/MetadataResolvers/{resourceId}"() { + when: 'GET request is made with resource Id not matching any resolvers' + def result = this.restTemplate.getForEntity("$BASE_URI/bogus-resource-id", String) + + then: + result.statusCodeValue == 404 + } + + def "POST new DynamicHttpMetadataResolver -> /api/MetadataResolvers"() { + given: 'New MetadataResolver JSON representation' + def resolver = [name: 'Test DynamicHttpMetadataResolver', '@type': 'DynamicHttpMetadataResolver'] + + when: 'POST request is made with new DynamicHttpMetadataResolver JSON representation' + def result = this.restTemplate.postForEntity(BASE_URI, createRequestHttpEntityFor { JsonOutput.toJson(resolver) }, String) + + then: + result.statusCodeValue == 201 + result.headers.Location[0].contains(BASE_URI) + } + + def "PUT concrete MetadataResolver with updated changes -> /api/MetadataResolvers/{resourceId}"() { + given: 'One resolver is available in data store' + def resolver = new DynamicHttpMetadataResolver().with { + it.name = 'Test DynamicHttpMetadataResolver' + it + } + def resolverResourceId = resolver.resourceId + metadataResolverRepository.save(resolver) + + when: 'GET request is made with resource Id matching the existing resolver' + def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) + + and: 'Resolver data is updated and sent back to the server' + def metadataResolverMap = new JsonSlurper().parseText(result.body) + metadataResolverMap.name = 'Updated DynamicHttpMetadataResolver' + def updatedResult = this.restTemplate.exchange( + "$BASE_URI/${metadataResolverMap.resourceId}", + PUT, + createRequestHttpEntityFor { JsonOutput.toJson(metadataResolverMap) }, + String) + then: + updatedResult.statusCodeValue == 200 + + and: + def updatedResolverMap = new JsonSlurper().parseText(updatedResult.body) + + then: + updatedResolverMap.name == 'Updated DynamicHttpMetadataResolver' + + } + + def "PUT concrete MetadataResolver with version conflict -> /api/MetadataResolvers/{resourceId}"() { + given: 'One resolver is available in data store' + def resolver = new DynamicHttpMetadataResolver().with { + it.name = 'Test DynamicHttpMetadataResolver' + it + } + def resolverResourceId = resolver.resourceId + def persistedResolver = metadataResolverRepository.save(resolver) + + when: 'GET request is made with resource Id matching the existing resolver' + def result = this.restTemplate.getForEntity("$BASE_URI/$resolverResourceId", String) + + and: 'Resolver data is updated and sent back to the server, but then original resolver is changed in data store' + persistedResolver.name = 'Some other name' + metadataResolverRepository.save(persistedResolver) + def metadataResolverMap = new JsonSlurper().parseText(result.body) + metadataResolverMap.name = 'Updated DynamicHttpMetadataResolver' + def updatedResult = this.restTemplate.exchange( + "$BASE_URI/${metadataResolverMap.resourceId}", + PUT, + createRequestHttpEntityFor { JsonOutput.toJson(metadataResolverMap) }, + String) + + then: + updatedResult.statusCodeValue == 409 + } + + private HttpEntity createRequestHttpEntityFor(Closure jsonBodySupplier) { + new HttpEntity(jsonBodySupplier(), ['Content-Type': 'application/json'] as HttpHeaders) + } + + @TestConfiguration + static class Config { + @Bean + MetadataResolver metadataResolver() { + new FilesystemMetadataResolver(new File('fake')) + } + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTargetTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTargetTests.groovy similarity index 93% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTargetTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTargetTests.groovy index cf4fd32eb..2ff346ee7 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/EntityAttributesFilterTargetTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/EntityAttributesFilterTargetTests.groovy @@ -1,4 +1,4 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain +package edu.internet2.tier.shibboleth.admin.ui.domain.filters import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import spock.lang.Specification diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/PolymorphicFiltersJacksonHandlingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy similarity index 99% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/PolymorphicFiltersJacksonHandlingTests.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy index 4e5899f49..56112a350 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/PolymorphicFiltersJacksonHandlingTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/filters/PolymorphicFiltersJacksonHandlingTests.groovy @@ -1,4 +1,4 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain +package edu.internet2.tier.shibboleth.admin.ui.domain.filters import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature 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/MetadataResolverValidationServiceConfigurationTests.groovy new file mode 100644 index 000000000..9007cb15e --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceConfigurationTests.groovy @@ -0,0 +1,32 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers + +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestMetadataResolverValidationConfiguration +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author Dmitriy Kopylenko + */ +@ContextConfiguration(classes=[TestMetadataResolverValidationConfiguration]) +class MetadataResolverValidationServiceConfigurationTests extends Specification { + + @Autowired + @Qualifier("metadataResolverValidationServiceEmpty") + MetadataResolverValidationService metadataResolverValidationServiceNoValidators + + @Autowired + @Qualifier("metadataResolverValidationServiceOneValidator") + MetadataResolverValidationService metadataResolverValidationServiceOneValidator + + def "Validation service with no validators"() { + expect: + metadataResolverValidationServiceNoValidators.noValidatorsConfigured() + } + + def "Validation service with one validator"() { + expect: + !metadataResolverValidationServiceOneValidator.noValidatorsConfigured() + } +} 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/MetadataResolverValidationServiceTests.groovy new file mode 100644 index 000000000..d62f07170 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolverValidationServiceTests.groovy @@ -0,0 +1,93 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers + +import spock.lang.Specification +import spock.lang.Subject + +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.* + +/** + * @author Dmitriy Kopylenko + */ +class MetadataResolverValidationServiceTests extends Specification { + + def "Validation service with no available validators always returns default valid result"() { + given: 'Sample metadata resolver and validation service with no validators' + def resolver = Mock(MetadataResolver) + @Subject + def validationService = new MetadataResolverValidationService(null) + + when: 'Validation call is made' + def validationResult = validationService.validateIfNecessary(resolver) + + then: + validationResult.valid + } + + 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) + validator.supports(_) >> false + @Subject + def validationService = new MetadataResolverValidationService([validator]) + + when: 'Validation call is made' + def validationResult = validationService.validateIfNecessary(resolver) + + then: + validationResult.valid + } + + 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) + validator.supports(_) >> true + validator.validate(_) >> new ValidationResult('Invalid') + @Subject + def validationService = new MetadataResolverValidationService([validator]) + + when: 'Validation call is made' + def validationResult = validationService.validateIfNecessary(resolver) + + then: + !validationResult.valid + } + + 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) + validator1.supports(_) >> true + validator1.validate(_) >> new ValidationResult('Invalid') + def validator2 = Mock(MetadataResolverValidator) + validator2.supports(_) >> true + validator2.validate(_) >> new ValidationResult(null) + @Subject + def validationService = new MetadataResolverValidationService([validator1, validator2]) + + when: 'Validation call is made' + def validationResult = validationService.validateIfNecessary(resolver) + + then: 'Result depends on list order of validators if all of them support the resolver type. This would be considered a mis-configuration' + !validationResult.valid + } + + 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) + validator1.supports(_) >> false + def validator2 = Mock(MetadataResolverValidator) + validator2.supports(_) >> true + validator2.validate(_) >> new ValidationResult(null) + @Subject + def validationService = new MetadataResolverValidationService([validator1, validator2]) + + when: 'Validation call is made' + def validationResult = validationService.validateIfNecessary(resolver) + + then: + validationResult.valid + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy new file mode 100644 index 000000000..79962f546 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/PolymorphicResolversJacksonHandlingTests.groovy @@ -0,0 +1,357 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +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.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import spock.lang.Specification + +class PolymorphicResolversJacksonHandlingTests extends Specification { + + ObjectMapper mapper + + AttributeUtility attributeUtility + + TestObjectGenerator testObjectGenerator + + def setup() { + mapper = new ObjectMapper() + mapper.enable(SerializationFeature.INDENT_OUTPUT) + + attributeUtility = new AttributeUtility(new OpenSamlObjects().with { + it.init() + it + }) + testObjectGenerator = new TestObjectGenerator(attributeUtility) + } + + def "Correct polymorphic serialization of LocalDynamicMetadataResolver"() { + given: + def givenResolverJson = """ + { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : null, + "requireValidMetadata" : true, + "failFastInitialization" : true, + "sortKey" : null, + "criterionPredicateRegistryRef" : null, + "useDefaultPredicateRegistry" : true, + "satisfyAnyPredicates" : false, + "metadataFilters" : [ { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityAttributes", + "filterEnabled" : false, + "version" : 463855403, + "entityAttributesFilterTarget" : { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "entityAttributesFilterTargetType" : "ENTITY", + "value" : [ "CedewbJJET" ], + "audId" : null + }, + "attributeRelease" : [ "9ktPyjjiCn" ], + "relyingPartyOverrides" : { + "signAssertion" : false, + "dontSignResponse" : true, + "turnOffEncryption" : true, + "useSha" : false, + "ignoreAuthenticationMethod" : false, + "omitNotBefore" : true, + "responderId" : null, + "nameIdFormats" : [ ], + "authenticationMethods" : [ ] + }, + "audId" : null, + "@type" : "EntityAttributes" + }, { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityRoleWhiteList", + "filterEnabled" : false, + "version" : 0, + "removeRolelessEntityDescriptors" : true, + "removeEmptyEntitiesDescriptors" : true, + "retainedRoles" : [ "role1", "role2" ], + "audId" : null, + "@type" : "EntityRoleWhiteList" + } ], + "version" : 0, + "sourceDirectory" : "dir", + "sourceManagerRef" : null, + "sourceKeyGeneratorRef" : null, + "audId" : null, + "@type" : "LocalDynamicMetadataResolver" + } + """ + + when: + def deSerializedResolver = mapper.readValue(givenResolverJson, MetadataResolver) + def json = mapper.writeValueAsString(deSerializedResolver) + println(json) + def roundTripResolver = mapper.readValue(json, MetadataResolver) + + + then: + roundTripResolver == deSerializedResolver + deSerializedResolver instanceof LocalDynamicMetadataResolver + deSerializedResolver.metadataFilters.size() == 2 + deSerializedResolver.metadataFilters[0] instanceof EntityAttributesFilter + deSerializedResolver.metadataFilters[1] instanceof EntityRoleWhiteListFilter + } + + def "Correct polymorphic serialization of DynamicHttpMetadataResolver"() { + given: + def givenResolverJson = """ + { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : null, + "resourceId" : "248168e2-7ccb-424e-8480-263f3df32034", + "requireValidMetadata" : true, + "failFastInitialization" : true, + "sortKey" : null, + "criterionPredicateRegistryRef" : null, + "useDefaultPredicateRegistry" : true, + "satisfyAnyPredicates" : false, + "metadataFilters" : [ { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityAttributes", + "resourceId" : "3bc95d7d-4db6-4f56-a99b-8da2163a936c", + "filterEnabled" : false, + "version" : -107872130, + "entityAttributesFilterTarget" : { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "entityAttributesFilterTargetType" : "ENTITY", + "value" : [ "ohD3PFTEoJ" ], + "audId" : null + }, + "attributeRelease" : [ "CsYlANiTHt" ], + "relyingPartyOverrides" : { + "signAssertion" : true, + "dontSignResponse" : false, + "turnOffEncryption" : true, + "useSha" : true, + "ignoreAuthenticationMethod" : true, + "omitNotBefore" : true, + "responderId" : "310c2919-ee78-4c61-8f03-b653212cfdfb", + "nameIdFormats" : [ "cYAVB5imC2" ], + "authenticationMethods" : [ ] + }, + "audId" : null, + "@type" : "EntityAttributes" + }, { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityRoleWhiteList", + "resourceId" : "e68c84c5-c1d9-45c9-bddc-336b55ebe3ed", + "filterEnabled" : false, + "version" : 0, + "removeRolelessEntityDescriptors" : true, + "removeEmptyEntitiesDescriptors" : true, + "retainedRoles" : [ "role1", "role2" ], + "audId" : null, + "@type" : "EntityRoleWhiteList" + } ], + "version" : 0, + "dynamicMetadataResolverAttributes" : { + "parserPoolRef" : null, + "taskTimerRef" : null, + "refreshDelayFactor" : 0.75, + "minCacheDuration" : "PT10M", + "maxCacheDuration" : "PT8H", + "maxIdleEntityData" : "PT8H", + "removeIdleEntityData" : null, + "cleanupTaskInterval" : "PT30M", + "persistentCacheManagerRef" : null, + "persistentCacheManagerDirectory" : null, + "persistentCacheKeyGeneratorRef" : null, + "initializeFromPersistentCacheInBackground" : true, + "backgroundInitializationFromCacheDelay" : "PT2S", + "initializationFromCachePredicateRef" : null + }, + "httpMetadataResolverAttributes" : { + "httpClientRef" : null, + "connectionRequestTimeout" : null, + "connectionTimeout" : null, + "socketTimeout" : null, + "disregardTLSCertificate" : false, + "tlsTrustEngineRef" : null, + "httpClientSecurityParametersRef" : null, + "proxyHost" : null, + "proxyPort" : null, + "proxyUser" : null, + "proxyPassword" : null, + "httpCaching" : null, + "httpCacheDirectory" : null, + "httpMaxCacheEntries" : null, + "httpMaxCacheEntrySize" : null + }, + "maxConnectionsTotal" : 100, + "maxConnectionsPerRoute" : 100, + "supportedContentTypes" : null, + "audId" : null, + "@type" : "DynamicHttpMetadataResolver" + } + """ + + when: + def deSerializedResolver = mapper.readValue(givenResolverJson, MetadataResolver) + def json = mapper.writeValueAsString(deSerializedResolver) + println(json) + def roundTripResolver = mapper.readValue(json, MetadataResolver) + + + then: + roundTripResolver == deSerializedResolver + deSerializedResolver instanceof DynamicHttpMetadataResolver + deSerializedResolver.metadataFilters.size() == 2 + deSerializedResolver.metadataFilters[0] instanceof EntityAttributesFilter + deSerializedResolver.metadataFilters[1] instanceof EntityRoleWhiteListFilter + } + + def "Correct polymorphic serialization of FileBackedHttpMetadataResolver"() { + given: + MetadataResolver resolver = new FileBackedHttpMetadataResolver().with { + it.httpMetadataResolverAttributes = new HttpMetadataResolverAttributes() + it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes() + it.metadataFilters = [testObjectGenerator.entityAttributesFilter(), testObjectGenerator.entityRoleWhitelistFilter()] + it + } + def givenResolverJson = """ + { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : null, + "resourceId" : "f3e615d5-960b-4fed-bff6-86fc4620be95", + "requireValidMetadata" : true, + "failFastInitialization" : true, + "sortKey" : null, + "criterionPredicateRegistryRef" : null, + "useDefaultPredicateRegistry" : true, + "satisfyAnyPredicates" : false, + "metadataFilters" : [ { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityAttributes", + "resourceId" : "4149cc5f-137e-4045-9369-8fedafcdd8c8", + "filterEnabled" : false, + "version" : -1249726767, + "entityAttributesFilterTarget" : { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "entityAttributesFilterTargetType" : "CONDITION_SCRIPT", + "value" : [ "6EksoLF7Q0" ], + "audId" : null + }, + "attributeRelease" : [ ], + "relyingPartyOverrides" : { + "signAssertion" : false, + "dontSignResponse" : true, + "turnOffEncryption" : false, + "useSha" : false, + "ignoreAuthenticationMethod" : false, + "omitNotBefore" : false, + "responderId" : "3267361e-7d8c-45d2-92ce-7642dc3bb432", + "nameIdFormats" : [ "baHO7CzFHH" ], + "authenticationMethods" : [ ] + }, + "audId" : null, + "@type" : "EntityAttributes" + }, { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityRoleWhiteList", + "resourceId" : "75117ec7-c74a-45cb-b216-cbbc9118fe70", + "filterEnabled" : false, + "version" : 0, + "removeRolelessEntityDescriptors" : true, + "removeEmptyEntitiesDescriptors" : true, + "retainedRoles" : [ "role1", "role2" ], + "audId" : null, + "@type" : "EntityRoleWhiteList" + } ], + "version" : 0, + "metadataURL" : null, + "backingFile" : null, + "initializeFromBackupFile" : true, + "backupFileInitNextRefreshDelay" : null, + "reloadableMetadataResolverAttributes" : { + "parserPoolRef" : null, + "taskTimerRef" : null, + "minRefreshDelay" : null, + "maxRefreshDelay" : null, + "refreshDelayFactor" : null, + "indexesRef" : null, + "resolveViaPredicatesOnly" : null, + "expirationWarningThreshold" : null + }, + "httpMetadataResolverAttributes" : { + "httpClientRef" : null, + "connectionRequestTimeout" : null, + "connectionTimeout" : null, + "socketTimeout" : null, + "disregardTLSCertificate" : false, + "tlsTrustEngineRef" : null, + "httpClientSecurityParametersRef" : null, + "proxyHost" : null, + "proxyPort" : null, + "proxyUser" : null, + "proxyPassword" : null, + "httpCaching" : null, + "httpCacheDirectory" : null, + "httpMaxCacheEntries" : null, + "httpMaxCacheEntrySize" : null + }, + "audId" : null, + "@type" : "FileBackedHttpMetadataResolver" + } + """ + + when: + //println mapper.writeValueAsString(resolver) + def deSerializedResolver = mapper.readValue(givenResolverJson, MetadataResolver) + def json = mapper.writeValueAsString(deSerializedResolver) + println(json) + def roundTripResolver = mapper.readValue(json, MetadataResolver) + + + then: + true + roundTripResolver == deSerializedResolver + deSerializedResolver instanceof FileBackedHttpMetadataResolver + deSerializedResolver.metadataFilters.size() == 2 + deSerializedResolver.metadataFilters[0] instanceof EntityAttributesFilter + deSerializedResolver.metadataFilters[1] instanceof EntityRoleWhiteListFilter + } +} 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/ResourceBackedMetadataValidatorTests.groovy new file mode 100644 index 000000000..6447a7f1a --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ResourceBackedMetadataValidatorTests.groovy @@ -0,0 +1,52 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers + +import spock.lang.Specification + +class ResourceBackedMetadataValidatorTests extends Specification { + + def "Does not support foreign resolver type"() { + given: + MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + FileBackedHttpMetadataResolver resolver = new FileBackedHttpMetadataResolver() + + expect: + !validator.supports(resolver) + } + + def "Passes validation"() { + given: + MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver().with { + it.classpathMetadataResource = new ClasspathMetadataResource() + it + } + + expect: + validator.supports(resolver) + validator.validate(resolver).valid + } + + def "Does not pass validation with both resource types missing"() { + given: + MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver() + + expect: + validator.supports(resolver) + !validator.validate(resolver).valid + } + + def "Does not pass validation with both resource types present"() { + given: + MetadataResolverValidator validator = new ResourceBackedMetadataResolverValidator() + ResourceBackedMetadataResolver resolver = new ResourceBackedMetadataResolver().with { + it.classpathMetadataResource = new ClasspathMetadataResource() + it.svnMetadataResource = new SvnMetadataResource() + it + } + + expect: + validator.supports(resolver) + !validator.validate(resolver).valid + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy index 8935e2eac..e10ff5b0b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy @@ -11,7 +11,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.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.DirtiesContext + import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -24,7 +24,6 @@ import javax.persistence.EntityManager @ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) class EnityDescriptorRepositoryTest extends Specification { @Autowired EntityDescriptorRepository entityDescriptorRepository diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy index 619b650f9..5b5d90357 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy @@ -84,6 +84,7 @@ class FileBackedHttpMetadataResolverRepositoryTests extends Specification { // I suspect similar weirdness if httpMetadataResolverAttributes is an empty object, too. def resolverJson = '''{ "name": "name", + "@type": "FileBackedHttpMetadataResolver", "requireValidMetadata": true, "failFastInitialization": true, "sortKey": 7, diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy index 62b13ca8b..3ef170b9d 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy @@ -2,24 +2,22 @@ package edu.internet2.tier.shibboleth.admin.ui.repository import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration 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.filters.EntityAttributesFilter 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.test.annotation.DirtiesContext import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import javax.persistence.EntityManager @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) class FilterRepositoryTests extends Specification { @Autowired diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy index 027b9c030..039f3b324 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy @@ -1,8 +1,8 @@ package edu.internet2.tier.shibboleth.admin.ui.repository import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration -import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration 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.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicMetadataResolverAttributes @@ -11,7 +11,6 @@ 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.test.annotation.DirtiesContext import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -20,10 +19,9 @@ import javax.persistence.EntityManager import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) class LocalDynamicMetadataResolverRepositoryTests extends Specification { @Autowired diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy similarity index 74% rename from backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTest.groovy rename to backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy index cf87e218c..7179dceee 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy @@ -6,6 +6,9 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl @@ -14,21 +17,19 @@ 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.test.annotation.DirtiesContext import org.springframework.test.context.ContextConfiguration import spock.lang.Specification import javax.persistence.EntityManager /** - * A highly unnecessary test so that I can check to make sure that persistence is correct for the model + * Testing persistence of the MetadataResolver models */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) -class MetadataResolverRepositoryTest extends Specification { +class MetadataResolverRepositoryTests extends Specification { @Autowired MetadataResolverRepository metadataResolverRepository @@ -42,44 +43,16 @@ class MetadataResolverRepositoryTest extends Specification { def "test persisting a metadata resolver"() { when: - def mdr = new MetadataResolver().with { - it.name = "testme" - it.metadataFilters.add(new EntityAttributesFilter().with { - it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { - it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY - it.value = ["hola"] - return it - } - return it - }) - return it - } + def mdr = create { new MetadataResolver() } metadataResolverRepository.save(mdr) then: - metadataResolverRepository.findAll().size() > 0 - MetadataResolver item = metadataResolverRepository.findByName("testme") - item.name == "testme" - item.metadataFilters.size() == 1 - item.metadataFilters.get(0).entityAttributesFilterTarget.entityAttributesFilterTargetType == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY - item.metadataFilters.get(0).entityAttributesFilterTarget.value.size() == 1 - item.metadataFilters.get(0).entityAttributesFilterTarget.value.get(0) == "hola" + basicPersistenceOfResolverIsCorrectFor { it instanceof MetadataResolver } } def "SHIBUI-553"() { when: - def mdr = new MetadataResolver().with { - it.name = "testme" - it.metadataFilters.add(new EntityAttributesFilter().with { - it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { - it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY - it.setSingleValue(["hola"]) - return it - } - return it - }) - return it - } + def mdr = create { new MetadataResolver() } metadataResolverRepository.save(mdr) def item1 = metadataResolverRepository.findByName('testme') @@ -200,4 +173,61 @@ class MetadataResolverRepositoryTest extends Specification { !persistedFilter.relyingPartyOverrides.signAssertion } + def "test persisting DynamicHttpMetadataResolver "() { + when: + def mdr = create { new DynamicHttpMetadataResolver() } + metadataResolverRepository.save(mdr) + + then: + basicPersistenceOfResolverIsCorrectFor { it instanceof DynamicHttpMetadataResolver } + } + + def "test persisting FileBackedHttpMetadataResolver "() { + when: + def mdr = create { new FileBackedHttpMetadataResolver() } + metadataResolverRepository.save(mdr) + + then: + basicPersistenceOfResolverIsCorrectFor { it instanceof FileBackedHttpMetadataResolver } + } + + def "test persisting LocalDynamicMetadataResolver "() { + when: + def mdr = create { new LocalDynamicMetadataResolver() } + metadataResolverRepository.save(mdr) + + then: + basicPersistenceOfResolverIsCorrectFor { it instanceof LocalDynamicMetadataResolver } + } + + + + private void basicPersistenceOfResolverIsCorrectFor(Closure resolverTypeCheck) { + assert metadataResolverRepository.findAll().size() > 0 + MetadataResolver item = metadataResolverRepository.findByName("testme") + assert resolverTypeCheck(item) + assert item.name == "testme" + assert item.metadataFilters.size() == 1 + assert item.metadataFilters.get(0).entityAttributesFilterTarget.entityAttributesFilterTargetType == EntityAttributesFilterTarget + .EntityAttributesFilterTargetType.ENTITY + assert item.metadataFilters.get(0).entityAttributesFilterTarget.value.size() == 1 + assert item.metadataFilters.get(0).entityAttributesFilterTarget.value.get(0) == "hola" + } + + private MetadataResolver create(Closure concreteResolverSupplier) { + MetadataResolver resolver = concreteResolverSupplier() + resolver.with { + it.name = "testme" + it.metadataFilters.add(new EntityAttributesFilter().with { + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY + it.value = ["hola"] + return it + } + return it + }) + } + resolver + } + } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy index 4e4600ae9..24a97ccdb 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy @@ -19,7 +19,6 @@ import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.TestConfiguration import org.springframework.context.annotation.Bean import org.springframework.data.jpa.repository.config.EnableJpaRepositories -import org.springframework.test.annotation.DirtiesContext import org.springframework.test.context.ContextConfiguration import spock.lang.Specification @@ -31,7 +30,6 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.* @ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") -@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverService metadataResolverService @@ -42,6 +40,10 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired AttributeUtility attributeUtility + def cleanup() { + metadataResolverRepository.deleteAll() + } + def 'simple test generation of metadata-providers.xml'() { when: def mr = metadataResolverRepository.findAll().iterator().next() @@ -137,6 +139,9 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { // Generate and test edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver. metadataResolverRepository.save(new TestObjectGenerator(attributeUtility).localDynamicMetadataResolver()) + + // Generate and test edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver + metadataResolverRepository.save(new TestObjectGenerator(attributeUtility).resourceBackedMetadataResolverForSVN()) } return resolver diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index e548d26b5..03b737da9 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -5,6 +5,8 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.domain.filters.RequiredValidUntilFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ClasspathMetadataResource +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.SvnMetadataResource import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository @@ -80,6 +82,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { } def cleanup() { + metadataResolverRepository.deleteAll() writer.close() } @@ -170,6 +173,48 @@ class JPAMetadataResolverServiceImplTests extends Specification { generatedXmlIsTheSameAsExpectedXml('/conf/532.xml', domBuilder.parseText(writer.toString())) } + def 'test generating ResourceBackedMetadataResolver with SVN resource type xml snippet'() { + given: + def resolver = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver().with { + it.name = 'SVNResourceMetadata' + it.svnMetadataResource = new SvnMetadataResource().with { + it.resourceFile = 'entity.xml' + it.repositoryURL = 'https://svn.example.org/repo/path/to.dir' + it.workingCopyDirectory = '%{idp.home}/metadata/svn' + it + } + it + } + + when: + genXmlSnippet(markupBuilder) { + JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForResolver(resolver, it) {} + } + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/546-svn.xml', domBuilder.parseText(writer.toString())) + } + + def 'test generating ResourceBackedMetadataResolver with classpath resource type xml snippet'() { + given: + def resolver = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver().with { + it.name = 'ClasspathResourceMetadata' + it.classpathMetadataResource = new ClasspathMetadataResource().with { + it.file = '/path/to/a/classpath/location/metadata.xml' + it + } + it + } + + when: + genXmlSnippet(markupBuilder) { + JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForResolver(resolver, it) {} + } + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/546-classpath.xml', domBuilder.parseText(writer.toString())) + } + static genXmlSnippet(MarkupBuilder xml, Closure xmlNodeGenerator) { xml.MetadataProvider('id': 'ShibbolethMetadata', 'xmlns': 'urn:mace:shibboleth:2.0:metadata', 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 fe5be3277..77e96e82d 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 @@ -406,6 +406,19 @@ class TestObjectGenerator { } } + ResourceBackedMetadataResolver resourceBackedMetadataResolverForSVN() { + new ResourceBackedMetadataResolver().with { + it.name = 'SVNResourceMetadata' + it.svnMetadataResource = new SvnMetadataResource().with { + it.resourceFile = 'entity.xml' + it.repositoryURL = 'https://svn.example.org/repo/path/to.dir' + it.workingCopyDirectory = '%{idp.home}/metadata/svn' + it + } + it + } + } + FileBackedHttpMetadataResolver buildFileBackedHttpMetadataResolver() { def resolver = new FileBackedHttpMetadataResolver() resolver.name = generator.randomString(10) diff --git a/backend/src/test/resources/conf/278.2.xml b/backend/src/test/resources/conf/278.2.xml index bd6c3b082..216f9626c 100644 --- a/backend/src/test/resources/conf/278.2.xml +++ b/backend/src/test/resources/conf/278.2.xml @@ -52,5 +52,13 @@ xsi:type="DynamicHttpMetadataProvider"> + + + + \ No newline at end of file diff --git a/backend/src/test/resources/conf/278.xml b/backend/src/test/resources/conf/278.xml index 7241943ab..6f845bf59 100644 --- a/backend/src/test/resources/conf/278.xml +++ b/backend/src/test/resources/conf/278.xml @@ -45,4 +45,12 @@ xsi:type="DynamicHttpMetadataProvider"> + + + + \ No newline at end of file diff --git a/backend/src/test/resources/conf/546-classpath.xml b/backend/src/test/resources/conf/546-classpath.xml new file mode 100644 index 000000000..13cba309c --- /dev/null +++ b/backend/src/test/resources/conf/546-classpath.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/backend/src/test/resources/conf/546-svn.xml b/backend/src/test/resources/conf/546-svn.xml new file mode 100644 index 000000000..579c7dc78 --- /dev/null +++ b/backend/src/test/resources/conf/546-svn.xml @@ -0,0 +1,16 @@ + + + + + + + +