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 da474a717..72fd0b2e6 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 @@ -1,28 +1,29 @@ package edu.internet2.tier.shibboleth.admin.ui.service; -import com.google.common.base.Predicate; -import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; +import com.google.common.base.Predicate +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import groovy.util.logging.Slf4j import groovy.xml.DOMBuilder -import groovy.xml.MarkupBuilder; -import net.shibboleth.utilities.java.support.resolver.ResolverException; -import org.opensaml.saml.common.profile.logic.EntityIdPredicate; -import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver; -import org.opensaml.saml.metadata.resolver.MetadataResolver; -import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver; -import org.opensaml.saml.metadata.resolver.filter.MetadataFilter; -import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain; -import org.opensaml.saml.saml2.core.Attribute; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.w3c.dom.Document; - -public class JPAMetadataResolverServiceImpl implements MetadataResolverService { - private static final Logger logger = LoggerFactory.getLogger(JPAMetadataResolverServiceImpl.class); +import groovy.xml.MarkupBuilder +import net.shibboleth.utilities.java.support.resolver.ResolverException +import org.opensaml.saml.common.profile.logic.EntityIdPredicate +import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver +import org.opensaml.saml.metadata.resolver.MetadataResolver +import org.opensaml.saml.metadata.resolver.RefreshableMetadataResolver +import org.opensaml.saml.metadata.resolver.filter.MetadataFilter +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain +import org.opensaml.saml.saml2.core.Attribute +import org.opensaml.saml.saml2.metadata.EntityDescriptor + +import org.springframework.beans.factory.annotation.Autowired +import org.w3c.dom.Document + +@Slf4j +class JPAMetadataResolverServiceImpl implements MetadataResolverService { @Autowired private MetadataResolver metadataResolver @@ -35,52 +36,52 @@ public class JPAMetadataResolverServiceImpl implements MetadataResolverService { // TODO: enhance @Override - public void reloadFilters(String metadataResolverName) { - ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver)metadataResolver; - - // MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().stream().filter(r -> r.getId().equals(metadataResolverName)).findFirst().get(); + void reloadFilters(String metadataResolverName) { + ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver)metadataResolver MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { it.id == metadataResolverName } - edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByName(metadataResolverName); + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByName(metadataResolverName) if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { - MetadataFilterChain metadataFilterChain = (MetadataFilterChain)targetMetadataResolver.getMetadataFilter(); + MetadataFilterChain metadataFilterChain = (MetadataFilterChain)targetMetadataResolver.getMetadataFilter() - List metadataFilters = new ArrayList<>(); + List metadataFilters = new ArrayList<>() for (edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter metadataFilter : jpaMetadataResolver.getMetadataFilters()) { if (metadataFilter instanceof EntityAttributesFilter) { - EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter; + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) metadataFilter - org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter(); - Map, Collection> rules = new HashMap<>(); + org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter target = new org.opensaml.saml.metadata.resolver.filter.impl.EntityAttributesFilter() + Map, Collection> rules = new HashMap<>() if (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType() == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY) { rules.put( new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), (List)(List)entityAttributesFilter.getAttributes() - ); + ) } - target.setRules(rules); - metadataFilters.add(target); + target.setRules(rules) + metadataFilters.add(target) } } - metadataFilterChain.setFilters(metadataFilters); + metadataFilterChain.setFilters(metadataFilters) } if (metadataResolver instanceof RefreshableMetadataResolver) { try { - ((RefreshableMetadataResolver)metadataResolver).refresh(); + ((RefreshableMetadataResolver)metadataResolver).refresh() } catch (ResolverException e) { - logger.warn("error refreshing metadataResolver " + metadataResolverName, e); + log.warn("error refreshing metadataResolver " + metadataResolverName, e) } } } // TODO: enhance @Override - public Document generateConfiguration() { + Document generateConfiguration() { // TODO: this can probably be a better writer new StringWriter().withCloseable { writer -> def xml = new MarkupBuilder(writer) + xml.omitEmptyAttributes = true + xml.omitNullAttributes = true xml.MetadataProvider(id: 'ShibbolethMetadata', xmlns: 'urn:mace:shibboleth:2.0:metadata', @@ -88,15 +89,8 @@ public class JPAMetadataResolverServiceImpl implements MetadataResolverService { 'xsi:type': 'ChainingMetadataProvider', 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' ) { - metadataResolverRepository.findAll().each { edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver mr -> - MetadataProvider(id: 'HTTPMetadata', - 'xsi:type': 'FileBackedHTTPMetadataProvider', - backingFile: '%{idp.home}/metadata/incommonmd.xml', - metadataURL: 'http://md.incommon.org/InCommon/InCommon-metadata.xml', - minRefreshDelay: 'PT5M', - maxRefreshDelay: 'PT1H', - refreshDelayFactor: '0.75' - ) { + metadataResolverRepository.findAll().each { edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> + constructXmlNodeForResolver(mr, delegate) { MetadataFilter( 'xsi:type': 'SignatureValidation', 'requireSignedRoot': 'true', @@ -114,7 +108,7 @@ public class JPAMetadataResolverServiceImpl implements MetadataResolverService { //TODO: enhance mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter filter -> if (filter instanceof EntityAttributesFilter) { - EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter)filter + EntityAttributesFilter entityAttributesFilter = (EntityAttributesFilter) filter MetadataFilter('xsi:type': 'EntityAttributes') { // TODO: enhance. currently this does weird things with namespaces entityAttributesFilter.attributes.each { attribute -> @@ -131,8 +125,49 @@ public class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } } - return DOMBuilder.newInstance().parseText(writer.toString()) } } + + void constructXmlNodeForResolver(FileBackedHttpMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { + markupBuilderDelegate.MetadataProvider(id: resolver.name, + 'xsi:type': 'FileBackedHTTPMetadataProvider', + backingFile: resolver.backingFile, + metadataURL: resolver.metadataURL, + initializeFromBackupFile: !resolver.initializeFromBackupFile ?: null, + backupFileInitNextRefreshDelay: resolver.backupFileInitNextRefreshDelay, + requireValidMetadata: !resolver.requireValidMetadata ?: null, + failFastInitialization: !resolver.failFastInitialization ?: null, + sortKey: resolver.sortKey, + criterionPredicateRegistryRef: resolver.criterionPredicateRegistryRef, + useDefaultPredicateRegistry: !resolver.useDefaultPredicateRegistry ?: null, + satisfyAnyPredicates: resolver.satisfyAnyPredicates ?: null, + + 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, + + httpClientRef: resolver.httpMetadataResolverAttributes?.httpClientRef, + connectionRequestTimeout: resolver.httpMetadataResolverAttributes?.connectionRequestTimeout, + connectionTimeout: resolver.httpMetadataResolverAttributes?.connectionTimeout, + socketTimeout: resolver.httpMetadataResolverAttributes?.socketTimeout, + disregardTLSCertificate: resolver.httpMetadataResolverAttributes?.disregardTLSCertificate ?: null, + httpClientSecurityParametersRef: resolver.httpMetadataResolverAttributes?.httpClientSecurityParametersRef, + proxyHost: resolver.httpMetadataResolverAttributes?.proxyHost, + proxyPort: resolver.httpMetadataResolverAttributes?.proxyHost, + proxyUser: resolver.httpMetadataResolverAttributes?.proxyUser, + proxyPassword: resolver.httpMetadataResolverAttributes?.proxyPassword, + httpCaching: resolver.httpMetadataResolverAttributes?.httpCaching, + httpCacheDirectory: resolver.httpMetadataResolverAttributes?.httpCacheDirectory, + httpMaxCacheEntries: resolver.httpMetadataResolverAttributes?.httpMaxCacheEntries, + httpMaxCacheEntrySize: resolver.httpMetadataResolverAttributes?.httpMaxCacheEntrySize) { + + childNodes() + } + } + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java index f6a88b27d..51ba89b05 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/MetadataResolverConfiguration.java @@ -95,7 +95,7 @@ protected void processConditionalRetrievalHeaders(HttpResponse response) { resolvers.add(incommonMR); if (!metadataResolverRepository.findAll().iterator().hasNext()) { - edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver mr = new edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver(); + edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver(); mr.setName("incommonmd"); metadataResolverRepository.save(mr); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FileBackedHttpMetadataProviderController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FileBackedHttpMetadataProviderController.java new file mode 100644 index 000000000..5b8000482 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FileBackedHttpMetadataProviderController.java @@ -0,0 +1,110 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.repository.FileBackedHttpMetadataResolverRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.DeleteMapping; +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; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@RestController +@RequestMapping("/api/MetadataProvider/FileBackedHttp") +public class FileBackedHttpMetadataProviderController { + private static final Logger logger = LoggerFactory.getLogger(FileBackedHttpMetadataProviderController.class); + + @Autowired + FileBackedHttpMetadataResolverRepository repository; + + @DeleteMapping("/{resourceId}") + public ResponseEntity deleteByResourceId(@PathVariable String resourceId) { + if (repository.deleteByResourceId(resourceId)) { + return ResponseEntity.accepted().build(); + } else { + return ResponseEntity.notFound().build(); + } + } + + @GetMapping("/name/{metadataProviderName}") + @Transactional(readOnly = true) + public ResponseEntity getOneByName(@PathVariable String metadataProviderName) { + FileBackedHttpMetadataResolver resolver = repository.findByName(metadataProviderName); + if (resolver == null) { + return ResponseEntity.notFound().build(); + } else { + resolver.setVersion(resolver.hashCode()); + return ResponseEntity.ok(resolver); + } + } + + @GetMapping("/{resourceId}") + @Transactional(readOnly = true) + public ResponseEntity getOneByResourceId(@PathVariable String resourceId) { + FileBackedHttpMetadataResolver resolver = repository.findByResourceId(resourceId); + if (resolver == null) { + return ResponseEntity.notFound().build(); + } else { + resolver.setVersion(resolver.hashCode()); + return ResponseEntity.ok(resolver); + } + } + + @PostMapping + public ResponseEntity create(@RequestBody FileBackedHttpMetadataResolver resolver) { + if (repository.findByName(resolver.getName()) != null) { + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + + FileBackedHttpMetadataResolver persistedResolver = repository.save(resolver); + persistedResolver.setVersion(persistedResolver.hashCode()); + + return ResponseEntity + .created(getResourceUriFor(persistedResolver)) + .body(persistedResolver); + } + + @PutMapping + public ResponseEntity update(@RequestBody FileBackedHttpMetadataResolver resolver) { + FileBackedHttpMetadataResolver existingResolver = repository.findByResourceId(resolver.getResourceId()); + + if (existingResolver == null) { + return ResponseEntity.notFound().build(); + } + + if (existingResolver.hashCode() != resolver.getVersion()) { + logger.info("Comparing: " + existingResolver.hashCode() + " with " + resolver.getVersion()); + return ResponseEntity.status(HttpStatus.CONFLICT).build(); + } + + resolver.setAudId(existingResolver.getAudId()); + //TODO: Do we need to set anything else? dates? + + FileBackedHttpMetadataResolver updatedResolver = repository.save(resolver); + updatedResolver.setVersion(updatedResolver.hashCode()); + + return ResponseEntity.ok(updatedResolver); + } + + private static URI getResourceUriFor(FileBackedHttpMetadataResolver resolver) { + return ServletUriComponentsBuilder + .fromCurrentServletMapping().path("/api/MetadataProvider/FileBackedHttp/") + .pathSegment(resolver.getResourceId()) + .build() + .toUri(); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FilterController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FilterController.java index 093a5b9ff..71db1e409 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FilterController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/FilterController.java @@ -2,7 +2,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter; -import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; import edu.internet2.tier.shibboleth.admin.ui.service.FilterService; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataResolver.java deleted file mode 100644 index 96a5526c0..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/MetadataResolver.java +++ /dev/null @@ -1,55 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.domain; - -import lombok.EqualsAndHashCode; - -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.OneToMany; -import javax.persistence.OrderColumn; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@Entity -@EqualsAndHashCode(callSuper = true) -public class MetadataResolver extends AbstractAuditable { - private String name; - private String resourceId = UUID.randomUUID().toString(); - - @OneToMany(cascade = CascadeType.ALL) - @OrderColumn - private List metadataFilters = new ArrayList<>(); - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getResourceId() { - return resourceId; - } - - public void setResourceId(String resourceId) { - this.resourceId = resourceId; - } - - public List getMetadataFilters() { - return metadataFilters; - } - - public void setMetadataFilters(List metadataFilters) { - this.metadataFilters = metadataFilters; - } - - @Override - public String toString() { - return "MetadataResolver{\n" + - "name='" + name + "\'\n" + - ", resourceId='" + resourceId + "\'\n" + - ", metadataFilters=\n" + metadataFilters + - '}'; - } -} 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 new file mode 100644 index 000000000..37748e6c0 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java @@ -0,0 +1,35 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.Embedded; +import javax.persistence.Entity; + +@Entity +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Getter +@Setter +@ToString +public class FileBackedHttpMetadataResolver extends MetadataResolver { + + private String metadataURL; + + private String backingFile; + + private Boolean initializeFromBackupFile = true; + + private String backupFileInitNextRefreshDelay; + + + @Embedded + private ReloadableMetadataResolverAttributes reloadableMetadataResolverAttributes; + + @Embedded + private HttpMetadataResolverAttributes httpMetadataResolverAttributes; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/HttpMetadataResolverAttributes.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/HttpMetadataResolverAttributes.java new file mode 100644 index 000000000..706c34e08 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/HttpMetadataResolverAttributes.java @@ -0,0 +1,58 @@ +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.Column; +import javax.persistence.Embeddable; +import javax.persistence.Enumerated; + +import static javax.persistence.EnumType.STRING; + +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +@EqualsAndHashCode +public class HttpMetadataResolverAttributes { + + private String httpClientRef; + + private String connectionRequestTimeout; + + private String connectionTimeout; + + private String socketTimeout; + + private Boolean disregardTLSCertificate; + + private String tlsTrustEngineRef; + + private String httpClientSecurityParametersRef; + + private String proxyHost; + + private String proxyPort; + + private String proxyUser; + + private String proxyPassword; + + @Enumerated(STRING) + @Column(length = 6) + private HttpCachingType httpCaching; + + private String httpCacheDirectory; + + private Integer httpMaxCacheEntries; + + private Integer httpMaxCacheEntrySize; + + public enum HttpCachingType { + none,file,memory + } +} 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 new file mode 100644 index 000000000..5efd6f5d2 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -0,0 +1,57 @@ +package edu.internet2.tier.shibboleth.admin.ui.domain.resolvers; + +import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; + +import javax.persistence.OneToMany; +import javax.persistence.OrderColumn; +import javax.persistence.Transient; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Entity +@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) +@EqualsAndHashCode(callSuper = true, exclude={"version"}) +@NoArgsConstructor +@Getter +@Setter +@ToString +public class MetadataResolver extends AbstractAuditable { + + @Column(unique=true) + private String name; + + @Column(unique=true) + private String resourceId = UUID.randomUUID().toString(); + + private Boolean requireValidMetadata = true; + + private Boolean failFastInitialization = true; + + private Integer sortKey; + + private String criterionPredicateRegistryRef; + + private Boolean useDefaultPredicateRegistry = true; + + private Boolean satisfyAnyPredicates; + + @OneToMany(cascade = CascadeType.ALL) + @OrderColumn + private List metadataFilters = new ArrayList<>(); + + @Transient + private int version; +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ReloadableMetadataResolverAttributes.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ReloadableMetadataResolverAttributes.java new file mode 100644 index 000000000..cddf5b935 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/ReloadableMetadataResolverAttributes.java @@ -0,0 +1,35 @@ +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 ReloadableMetadataResolverAttributes { + + private String parserPoolRef; + + private String taskTimerRef; + + private String minRefreshDelay; + + private String maxRefreshDelay; + + private Double refreshDelayFactor; + + private String indexesRef; + + private Boolean resolveViaPredicatesOnly; + + private String expirationWarningThreshold; + +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepository.java new file mode 100644 index 000000000..97d069173 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepository.java @@ -0,0 +1,14 @@ +package edu.internet2.tier.shibboleth.admin.ui.repository; + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver; +import org.springframework.data.repository.CrudRepository; + +/** + * Spring Data CRUD repository for instances of {@link FileBackedHttpMetadataResolver}s. + */ +public interface FileBackedHttpMetadataResolverRepository extends CrudRepository { + + FileBackedHttpMetadataResolver findByName(String name); + boolean deleteByResourceId(String resourceId); + FileBackedHttpMetadataResolver findByResourceId(String resourceId); +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java index 5dba01872..fc415aef1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepository.java @@ -1,10 +1,10 @@ package edu.internet2.tier.shibboleth.admin.ui.repository; -import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver; +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import org.springframework.data.repository.CrudRepository; /** - * Repository to manage {@link edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver} instances. + * Repository to manage {@link MetadataResolver} instances. */ public interface MetadataResolverRepository extends CrudRepository { MetadataResolver findByName(String name); diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FileBackedHttpMetadataProviderControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FileBackedHttpMetadataProviderControllerTests.groovy new file mode 100644 index 000000000..19975e843 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FileBackedHttpMetadataProviderControllerTests.groovy @@ -0,0 +1,275 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +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.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.FileBackedHttpMetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +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.context.ContextConfiguration +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import spock.lang.Specification + +import static org.hamcrest.CoreMatchers.containsString +import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8 +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class FileBackedHttpMetadataProviderControllerTests extends Specification { + RandomGenerator randomGenerator + TestObjectGenerator testObjectGenerator + ObjectMapper mapper + + @Autowired + AttributeUtility attributeUtility + + def repository = Mock(FileBackedHttpMetadataResolverRepository) + def controller + def mockMvc + + def setup() { + randomGenerator = new RandomGenerator() + testObjectGenerator = new TestObjectGenerator(attributeUtility) + mapper = new ObjectMapper() + + controller = new FileBackedHttpMetadataProviderController ( + repository: repository + ) + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } + + def "DELETE deletes the desired resolver"() { + given: + def randomResourceId = randomGenerator.randomId() + + 1 * repository.deleteByResourceId(randomResourceId) >> true + + when: + def result = mockMvc.perform( + delete("/api/MetadataProvider/FileBackedHttp/$randomResourceId")) + + then: + result.andExpect(status().isAccepted()) + } + + def "DELETE returns error when desired resolver is not found"() { + given: + def randomResourceId = randomGenerator.randomId() + + 1 * repository.deleteByResourceId(randomResourceId) >> false + + when: + def result = mockMvc.perform( + delete("/api/MetadataProvider/FileBackedHttp/$randomResourceId")) + + then: + result.andExpect(status().isNotFound()) + } + + def "POST a new resolver properly persists and returns the new persisted resolver"() { + given: + def resolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + resolver.version = resolver.hashCode() + def postedJsonBody = mapper.writeValueAsString(resolver) + + 1 * repository.findByName(resolver.getName()) >> null + 1 * repository.save(_) >> resolver + + def expectedResolverUUID = resolver.getResourceId() + def expectedResponseHeader = 'Location' + def expectedResponseHeaderValue = "/api/MetadataProvider/FileBackedHttp/$expectedResolverUUID" + + when: + def result = mockMvc.perform( + post('/api/MetadataProvider/FileBackedHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isCreated()) + .andExpect(content().json(postedJsonBody, false)) + .andExpect(header().string(expectedResponseHeader, containsString(expectedResponseHeaderValue))) + + } + + def "POST a new resolver that has a name of a persisted resolver returns conflict"() { + given: + def existingResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + def randomResolverName = existingResolver.name + def newResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + newResolver.name = randomResolverName + def postedJsonBody = mapper.writeValueAsString(newResolver) + + 1 * repository.findByName(randomResolverName) >> existingResolver + 0 * repository.save(_) + + when: + def result = mockMvc.perform( + post('/api/MetadataProvider/FileBackedHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isConflict()) + } + + def "GET by resourceId returns the desired persisted resolver"() { + given: + def existingResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + existingResolver.version = existingResolver.hashCode() + def randomResourceId = existingResolver.resourceId + def resolverJson = mapper.writeValueAsString(existingResolver) + + 1 * repository.findByResourceId(randomResourceId) >> existingResolver + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/FileBackedHttp/$randomResourceId")) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(resolverJson, false)) + } + + def "GET by unknown resource id returns not found"() { + given: + def randomResourceId = randomGenerator.randomId() + + 1 * repository.findByResourceId(randomResourceId) >> null + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/FileBackedHttp/$randomResourceId")) + + then: + result.andExpect(status().isNotFound()) + } + + def "GET by resolver name returns the desired persisted resolver"() { + given: + def randomResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + randomResolver.version = randomResolver.hashCode() + def randomResolverName = randomResolver.name + def resolverJson = mapper.writeValueAsString(randomResolver) + + 1 * repository.findByName(randomResolverName) >> randomResolver + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/FileBackedHttp/name/$randomResolverName")) + + then: + result.andExpect(status().isOk()) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(resolverJson, false)) + } + + def "GET by unknown resolver name returns not found"() { + given: + def randomResolverName = randomGenerator.randomString(10) + + 1 * repository.findByName(randomResolverName) >> null + + when: + def result = mockMvc.perform( + get("/api/MetadataProvider/FileBackedHttp/name/$randomResolverName")) + + then: + result.andExpect(status().isNotFound()) + } + + def "PUT allows for a successful update of an already-persisted resolver"() { + given: + def existingResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + existingResolver.version = existingResolver.hashCode() + def randomResourceId = existingResolver.resourceId + def updatedResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + updatedResolver.version = existingResolver.version + updatedResolver.resourceId = existingResolver.resourceId + def postedJsonBody = mapper.writeValueAsString(updatedResolver) + + + 1 * repository.findByResourceId(randomResourceId) >> existingResolver + 1 * repository.save(_) >> updatedResolver + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + + when: + def result = mockMvc.perform( + put('/api/MetadataProvider/FileBackedHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + def expectedJson = new JsonSlurper().parseText(postedJsonBody) + expectedJson << [version: updatedResolver.hashCode()] + result.andExpect(status().isOk()) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(JsonOutput.toJson(expectedJson), false)) + } + + def "PUT of an updated resolver with an incorrect version returns a conflict"() { + given: + def existingResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + existingResolver.version = existingResolver.hashCode() + def randomResourceId = existingResolver.resourceId + def updatedResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + updatedResolver.version = updatedResolver.hashCode() + updatedResolver.resourceId = existingResolver.resourceId + def postedJsonBody = mapper.writeValueAsString(updatedResolver) + + 1 * repository.findByResourceId(randomResourceId) >> existingResolver + 0 * repository.save(_) + + when: + def result = mockMvc.perform( + put('/api/MetadataProvider/FileBackedHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isConflict()) + } + + def "PUT of a resolver that is not persisted returns not found"() { + given: + def randomResolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + randomResolver.version = randomResolver.hashCode() + def randomResourceId = randomResolver.resourceId + def postedJsonBody = mapper.writeValueAsString(randomResolver) + + 1 * repository.findByResourceId(randomResourceId) >> null + 0 * repository.save(_) + + when: + def result = mockMvc.perform( + put('/api/MetadataProvider/FileBackedHttp') + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().isNotFound()) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FilterControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FilterControllerTests.groovy index fdf90d65d..9d8a1f72c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FilterControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/FilterControllerTests.groovy @@ -5,7 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfigurat 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.domain.EntityAttributesFilter -import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.service.FilterService import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService 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 new file mode 100644 index 000000000..c68667042 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy @@ -0,0 +1,128 @@ +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.domain.EntityAttributesFilter +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes +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 + +import static edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes.HttpCachingType.memory + +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +@DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) +class FileBackedHttpMetadataResolverRepositoryTests extends Specification { + + @Autowired + MetadataResolverRepository repositoryUnderTest + + @Autowired + EntityManager entityManager + + def "file backed http metadata resolver instances persist OK"() { + when: + def mdr = new FileBackedHttpMetadataResolver().with { + it.name = "FileBackedHttpMetadata" + + it.httpMetadataResolverAttributes = new HttpMetadataResolverAttributes().with { + it.connectionRequestTimeout = "PT05" + it.disregardTLSCertificate = true + it.httpCaching = memory + it + } + it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { + it.indexesRef = "indexesSpringBeanId" + it + } + + it.metadataFilters.add(new EntityAttributesFilter().with { + it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { + it.entityAttributesFilterTargetType = ENTITY + it.setValue(["hola"]) + it + } + it + }) + it + } + repositoryUnderTest.save(mdr) + + then: + repositoryUnderTest.findAll().size() > 0 + FileBackedHttpMetadataResolver item = repositoryUnderTest.findByName("FileBackedHttpMetadata") + + item.name == "FileBackedHttpMetadata" + item.metadataFilters.size() == 1 + item.metadataFilters[0].entityAttributesFilterTarget.entityAttributesFilterTargetType == ENTITY + item.metadataFilters[0].entityAttributesFilterTarget.value.size() == 1 + item.metadataFilters[0].entityAttributesFilterTarget.value.get(0) == "hola" + item.httpMetadataResolverAttributes.connectionRequestTimeout == "PT05" + item.httpMetadataResolverAttributes.disregardTLSCertificate + item.httpMetadataResolverAttributes.httpCaching == memory + item.reloadableMetadataResolverAttributes.indexesRef == "indexesSpringBeanId" + } + + def "FileBackedHttpMetadataResolver hashcode works as desired"() { + given: + // TODO: There is weirdness here if reloadableMetadataResolverAttributes is empty. + // I suspect similar weirdness if httpMetadataResolverAttributes is an empty object, too. + def resolverJson = '''{ + "name": "name", + "requireValidMetadata": true, + "failFastInitialization": true, + "sortKey": 7, + "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", + "useDefaultPredicateRegistry": true, + "satisfyAnyPredicates": true, + "metadataFilters": [], + "reloadableMetadataResolverAttributes": { + "parserPoolRef": "parserPoolRef" + }, + "httpMetadataResolverAttributes": { + "httpClientRef": "httpClientRef", + "connectionRequestTimeout": "connectionRequestTimeout", + "connectionTimeout": "connectionTimeout", + "socketTimeout": "socketTimeout", + "disregardTLSCertificate": true, + "tlsTrustEngineRef": "tlsTrustEngineRef", + "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", + "proxyHost": "proxyHost", + "proxyPort": "proxyPort", + "proxyUser": "proxyUser", + "proxyPassword": "proxyPassword", + "httpCaching": "none", + "httpCacheDirectory": "httpCacheDirectory", + "httpMaxCacheEntries": 1, + "httpMaxCacheEntrySize": 2 + } +}''' + + when: + def resolver = new ObjectMapper().readValue(resolverJson.bytes, FileBackedHttpMetadataResolver) + def persistedResolver = repositoryUnderTest.save(resolver) + entityManager.flush() + + then: + def item1 = repositoryUnderTest.findByName(persistedResolver.name) + entityManager.clear() + def item2 = repositoryUnderTest.findByName(persistedResolver.name) + + item1.hashCode() == item2.hashCode() + } +} 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/MetadataResolverRepositoryTest.groovy index 7e35f655e..76cb92e68 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/MetadataResolverRepositoryTest.groovy @@ -5,7 +5,7 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConf import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver +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 import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl 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 12112d0b9..ce88779f7 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 @@ -4,14 +4,12 @@ import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfigurat import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget -import edu.internet2.tier.shibboleth.admin.ui.domain.MetadataFilter import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility -import org.apache.http.impl.client.HttpClients import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver -import org.opensaml.saml.metadata.resolver.impl.FileBackedHTTPMetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -85,6 +83,9 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverRepository metadataResolverRepository + @Autowired + AttributeUtility attributeUtility + @Bean MetadataResolver metadataResolver() { def resolver = new ChainingMetadataResolver().with { @@ -100,8 +101,10 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { } if (!metadataResolverRepository.findAll().iterator().hasNext()) { - edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver mr = new edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver() - mr.setName("incommonmd") + //Generate and test edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver. Add more as + // we implement them + def mr = new TestObjectGenerator(attributeUtility).fileBackedHttpMetadataResolver() + mr.setName("HTTPMetadata") metadataResolverRepository.save(mr) } 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 faa913f6b..c6ee49871 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 @@ -1,12 +1,16 @@ package edu.internet2.tier.shibboleth.admin.ui.service 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.domain.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository + +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import groovy.xml.DOMBuilder +import groovy.xml.MarkupBuilder import net.shibboleth.ext.spring.resource.ResourceHelper import net.shibboleth.utilities.java.support.resolver.CriteriaSet import org.joda.time.DateTime @@ -29,6 +33,8 @@ import org.xmlunit.builder.DiffBuilder import org.xmlunit.builder.Input import spock.lang.Specification +import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml + @SpringBootTest @DataJpaTest @ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration]) @@ -51,6 +57,30 @@ class JPAMetadataResolverServiceImplTests extends Specification { @Autowired OpenSamlObjects openSamlObjects + @Autowired + AttributeUtility attributeUtility + + TestObjectGenerator testObjectGenerator + + DOMBuilder domBuilder + + StringWriter writer + + MarkupBuilder markupBuilder + + def setup() { + testObjectGenerator = new TestObjectGenerator(attributeUtility) + domBuilder = DOMBuilder.newInstance() + writer = new StringWriter() + markupBuilder = new MarkupBuilder(writer) + markupBuilder.omitNullAttributes = true + markupBuilder.omitEmptyAttributes = true + } + + def cleanup() { + writer.close() + } + def 'test adding a filter'() { given: def expectedXML = ''' @@ -75,7 +105,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { ''' when: - def mdr = new edu.internet2.tier.shibboleth.admin.ui.domain.MetadataResolver().with { + def mdr = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver().with { it.name = "testme" it.metadataFilters.add(new EntityAttributesFilter().with { it.entityAttributesFilterTarget = new EntityAttributesFilterTarget().with { @@ -99,6 +129,30 @@ class JPAMetadataResolverServiceImplTests extends Specification { !diff.hasDifferences() } + def 'test generating FileBackedHttMetadataResolver xml snippet'() { + given: + def resolver = testObjectGenerator.fileBackedHttpMetadataResolver() + + when: + genXmlSnippet(markupBuilder) { + JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForResolver(resolver, it) {} + } + + then: + assert generatedXmlIsTheSameAsExpectedXml('/conf/532.xml', domBuilder.parseText(writer.toString())) + } + + static genXmlSnippet(MarkupBuilder xml, Closure xmlNodeGenerator) { + xml.MetadataProvider('id': 'ShibbolethMetadata', + 'xmlns': 'urn:mace:shibboleth:2.0:metadata', + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:type': 'ChainingMetadataProvider', + 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' + ) { + xmlNodeGenerator(delegate) + } + } + @TestConfiguration static class Config { @Autowired diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy index 1a6a085e2..bbac2be5f 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy @@ -2,6 +2,9 @@ package edu.internet2.tier.shibboleth.admin.ui.util import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation import org.apache.commons.lang.StringUtils +import org.w3c.dom.Document +import org.xmlunit.builder.DiffBuilder +import org.xmlunit.builder.Input /** * @author Bill Smith (wsmith@unicon.net) @@ -22,4 +25,9 @@ class TestHelpers { return count } + + static generatedXmlIsTheSameAsExpectedXml(String expectedXmlResource, Document generatedXml) { + !DiffBuilder.compare(Input.fromStream(this.getResourceAsStream(expectedXmlResource))).withTest(Input.fromDocument(generatedXml)) + .ignoreComments().ignoreWhitespace().build().hasDifferences() + } } 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 302bdac95..da541f5ed 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 @@ -12,10 +12,16 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.OrganizationURL import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterTargetRepresentation import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ReloadableMetadataResolverAttributes import edu.internet2.tier.shibboleth.admin.util.AttributeUtility import edu.internet2.tier.shibboleth.admin.util.MDDCConstants import org.opensaml.saml.saml2.metadata.Organization +import static edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY +import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes.HttpCachingType.memory + /** * @author Bill Smith (wsmith@unicon.net) */ @@ -29,6 +35,28 @@ class TestObjectGenerator { this.attributeUtility = attributeUtility } + HttpMetadataResolverAttributes buildHttpMetadataResolverAttributes() { + def attributes = new HttpMetadataResolverAttributes().with { + it.disregardTLSCertificate = generator.randomBoolean() + it.connectionRequestTimeout = generator.randomString(10) + it.httpClientRef = generator.randomString(10) + it.httpCacheDirectory = generator.randomString(10) + it.httpCaching = randomHttpCachingType() + it.httpClientSecurityParametersRef = generator.randomString(10) + it.httpMaxCacheEntries = generator.randomInt(1, 10) + it.httpMaxCacheEntrySize = generator.randomInt(100, 10000) + it.proxyHost = generator.randomString(10) + it.proxyPassword = generator.randomString(10) + it.proxyPort = generator.randomString(5) + it.proxyUser = generator.randomString(10) + it.socketTimeout = generator.randomString(10) + it.tlsTrustEngineRef = generator.randomString(10) + it.connectionTimeout = generator.randomString(10) + it + } + return attributes + } + List buildFilterList() { List filterList = new ArrayList<>() (1..generator.randomInt(4, 10)).each { @@ -190,6 +218,54 @@ class TestObjectGenerator { return contactPerson } + FileBackedHttpMetadataResolver fileBackedHttpMetadataResolver() { + new FileBackedHttpMetadataResolver().with { + it.name = 'HTTPMetadata' + it.backingFile = '%{idp.home}/metadata/incommonmd.xml' + it.metadataURL = 'http://md.incommon.org/InCommon/InCommon-metadata.xml' + + it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { + it.minRefreshDelay = 'PT5M' + it.maxRefreshDelay = 'PT1H' + it.refreshDelayFactor = 0.75 + it + } + it + } + } + + FileBackedHttpMetadataResolver buildFileBackedHttpMetadataResolver() { + def resolver = new FileBackedHttpMetadataResolver() + resolver.name = generator.randomString(10) + resolver.requireValidMetadata = generator.randomBoolean() + resolver.failFastInitialization = generator.randomBoolean() + resolver.sortKey = generator.randomInt(0, 10) + resolver.criterionPredicateRegistryRef = generator.randomString(10) + resolver.useDefaultPredicateRegistry = generator.randomBoolean() + resolver.satisfyAnyPredicates = generator.randomBoolean() + resolver.metadataFilters = [] + resolver.reloadableMetadataResolverAttributes = buildReloadableMetadataResolverAttributes() + resolver.httpMetadataResolverAttributes = buildHttpMetadataResolverAttributes() + return resolver + } + + ReloadableMetadataResolverAttributes buildReloadableMetadataResolverAttributes() { + def attributes = new ReloadableMetadataResolverAttributes() + attributes.parserPoolRef = generator.randomString(10) + attributes.taskTimerRef = generator.randomString(10) + attributes.minRefreshDelay = generator.randomString(5) + attributes.maxRefreshDelay = generator.randomString(5) + attributes.refreshDelayFactor = generator.randomInt(0, 5) + attributes.indexesRef = generator.randomString(10) + attributes.resolveViaPredicatesOnly = generator.randomBoolean() + attributes.expirationWarningThreshold = generator.randomString(10) + return attributes + } + + HttpMetadataResolverAttributes.HttpCachingType randomHttpCachingType() { + HttpMetadataResolverAttributes.HttpCachingType.values()[generator.randomInt(0, 2)] + } + /** * This method takes a type and a size and builds a List of that size containing objects of that type. This is * intended to be used with things that extend LocalizedName such as {@link OrganizationName}, {@link OrganizationDisplayName}, diff --git a/backend/src/test/resources/conf/532.xml b/backend/src/test/resources/conf/532.xml new file mode 100644 index 000000000..cdfd93301 --- /dev/null +++ b/backend/src/test/resources/conf/532.xml @@ -0,0 +1,15 @@ + + + + +