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 2c873de69..d24174234 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 @@ -4,6 +4,9 @@ import com.google.common.base.Predicate 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.EntityRoleWhiteListFilter + +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 @@ -19,7 +22,8 @@ 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.springframework.beans.factory.annotation.Autowired + import org.w3c.dom.Document @Slf4j @@ -80,6 +84,8 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { // 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,14 +94,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { '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.resolvers.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' - ) { + constructXmlNodeForResolver(mr, delegate) { MetadataFilter( 'xsi:type': 'SignatureValidation', 'requireSignedRoot': 'true', @@ -107,17 +106,17 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { ) //TODO: enhance mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> - constructXmlNodeFor(filter, delegate) + constructXmlNodeForFilter(filter, delegate) } } } } - return DOMBuilder.newInstance().parseText(writer.toString()) } } - void constructXmlNodeFor(EntityAttributesFilter filter, def markupBuilderDelegate) { + + void constructXmlNodeForFilter(EntityAttributesFilter filter, def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter('xsi:type': 'EntityAttributes') { // TODO: enhance. currently this does weird things with namespaces filter.attributes.each { attribute -> @@ -132,7 +131,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } - void constructXmlNodeFor(EntityRoleWhiteListFilter filter, def markupBuilderDelegate) { + void constructXmlNodeForFilter(EntityRoleWhiteListFilter filter, def markupBuilderDelegate) { markupBuilderDelegate.MetadataFilter( 'xsi:type': 'EntityRoleWhiteList', 'xmlns:md': 'urn:oasis:names:tc:SAML:2.0:metadata' @@ -142,4 +141,45 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } } } + + 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/domain/resolvers/FileBackedHttpMetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/FileBackedHttpMetadataResolver.java index c9850ecc6..37748e6c0 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 @@ -17,6 +17,15 @@ @ToString public class FileBackedHttpMetadataResolver extends MetadataResolver { + private String metadataURL; + + private String backingFile; + + private Boolean initializeFromBackupFile = true; + + private String backupFileInitNextRefreshDelay; + + @Embedded private ReloadableMetadataResolverAttributes reloadableMetadataResolverAttributes; 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 index 437516ffd..706c34e08 100644 --- 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 @@ -24,7 +24,7 @@ public class HttpMetadataResolverAttributes { private String connectionRequestTimeout; - private String requestTimeout; + private String connectionTimeout; private String socketTimeout; @@ -52,7 +52,7 @@ public class HttpMetadataResolverAttributes { private Integer httpMaxCacheEntrySize; - private enum HttpCachingType { + 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 index ded0edd05..659380db6 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 @@ -35,15 +35,15 @@ public class MetadataResolver extends AbstractAuditable { @Column(unique=true) private String resourceId = UUID.randomUUID().toString(); - private Boolean requireValidMetadata; + private Boolean requireValidMetadata = true; - private Boolean failFastInitialization; + private Boolean failFastInitialization = true; private Integer sortKey; private String criterionPredicateRegistryRef; - private Boolean useDefaultPredicateRegistry; + private Boolean useDefaultPredicateRegistry = true; private Boolean satisfyAnyPredicates; 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 index 1cf30ab69..19975e843 100644 --- 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 @@ -7,8 +7,11 @@ 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 @@ -30,14 +33,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. @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 ( @@ -77,44 +85,10 @@ class FileBackedHttpMetadataProviderControllerTests extends Specification { def "POST a new resolver properly persists and returns the new persisted resolver"() { given: - def postedJsonBody = '''{ - "name": "name", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}''' - def resolver = new ObjectMapper().readValue(postedJsonBody.bytes, FileBackedHttpMetadataResolver) + def resolver = testObjectGenerator.buildFileBackedHttpMetadataResolver() + resolver.version = resolver.hashCode() + def postedJsonBody = mapper.writeValueAsString(resolver) + 1 * repository.findByName(resolver.getName()) >> null 1 * repository.save(_) >> resolver @@ -137,46 +111,13 @@ class FileBackedHttpMetadataProviderControllerTests extends Specification { def "POST a new resolver that has a name of a persisted resolver returns conflict"() { given: - def randomResolverName = randomGenerator.randomString(10) - def postedJsonBody = """{ - "name": "$randomResolverName", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" - def resolver = new ObjectMapper().readValue(postedJsonBody.bytes, FileBackedHttpMetadataResolver) - 1 * repository.findByName(randomResolverName) >> resolver + 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: @@ -191,56 +132,18 @@ class FileBackedHttpMetadataProviderControllerTests extends Specification { def "GET by resourceId returns the desired persisted resolver"() { given: - def randomUUID = randomGenerator.randomId() - def resolverJson = """{ - "name": "name", - "resourceId": "$randomUUID", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" - - def resolver = new ObjectMapper().readValue(resolverJson.bytes, FileBackedHttpMetadataResolver) - resolver.setResourceId(randomUUID) - - 1 * repository.findByResourceId(randomUUID) >> resolver + 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/$randomUUID")) + get("/api/MetadataProvider/FileBackedHttp/$randomResourceId")) then: result.andExpect(status().isOk()) @@ -264,48 +167,12 @@ class FileBackedHttpMetadataProviderControllerTests extends Specification { def "GET by resolver name returns the desired persisted resolver"() { given: - def randomResolverName = randomGenerator.randomString(10) - def resolverJson = """{ - "name": "$randomResolverName", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" - - def resolver = new ObjectMapper().readValue(resolverJson.bytes, FileBackedHttpMetadataResolver) - - 1 * repository.findByName(randomResolverName) >> resolver + 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 @@ -335,89 +202,15 @@ class FileBackedHttpMetadataProviderControllerTests extends Specification { def "PUT allows for a successful update of an already-persisted resolver"() { given: - def randomResourceId = "resourceId" - def existingResolverJson = """{ - "name": "name", - "resourceId": "$randomResourceId", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" - def existingResolver = new ObjectMapper().readValue(existingResolverJson.bytes, FileBackedHttpMetadataResolver) - def existingResolverVersion = existingResolver.hashCode() - - def randomName = randomGenerator.randomString(10) - def postedJsonBody = """{ - "name": "$randomName", - "resourceId": "$randomResourceId", - "version": "$existingResolverVersion", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" - def updatedResolver = new ObjectMapper().readValue(postedJsonBody.bytes, FileBackedHttpMetadataResolver) + 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 @@ -439,89 +232,14 @@ class FileBackedHttpMetadataProviderControllerTests extends Specification { def "PUT of an updated resolver with an incorrect version returns a conflict"() { given: - def randomResourceId = "resourceId" - def existingResolverJson = """{ - "name": "name", - "resourceId": "$randomResourceId", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" - def existingResolver = new ObjectMapper().readValue(existingResolverJson.bytes, FileBackedHttpMetadataResolver) - def existingResolverVersion = existingResolver.hashCode() - - def randomName = randomGenerator.randomString(10) - def randomVersion = randomGenerator.randomInt() - def postedJsonBody = """{ - "name": "$randomName", - "resourceId": "$randomResourceId", - "version": "$randomVersion", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" + 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(_) @@ -537,45 +255,11 @@ class FileBackedHttpMetadataProviderControllerTests extends Specification { def "PUT of a resolver that is not persisted returns not found"() { given: - def randomResourceId = randomGenerator.randomId() - def postedJsonBody = """{ - "name": "name", - "resourceId": "$randomResourceId", - "requireValidMetadata": true, - "failFastInitialization": true, - "sortKey": 7, - "criterionPredicateRegistryRef": "criterionPredicateRegistryRef", - "useDefaultPredicateRegistry": true, - "satisfyAnyPredicates": true, - "metadataFilters": [], - "reloadableMetadataResolverAttributes": { - "parserPoolRef": "parserPoolRef", - "taskTimerRef": "taskTimerRef", - "minRefreshDelay": "minRefreshDelay", - "maxRefreshDelay": "maxRefreshDelay", - "refreshDelayFactor": 1.0, - "indexesRef": "indexesRef", - "resolveViaPredicatesOnly": true, - "expirationWarningThreshold": "expirationWarningThreshold" - }, - "httpMetadataResolverAttributes": { - "httpClientRef": "httpClientRef", - "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", - "socketTimeout": "socketTimeout", - "disregardTLSCertificate": true, - "tlsTrustEngineRef": "tlsTrustEngineRef", - "httpClientSecurityParametersRef": "httpClientSecurityParametersRef", - "proxyHost": "proxyHost", - "proxyPort": "proxyPort", - "proxyUser": "proxyUser", - "proxyPassword": "proxyPassword", - "httpCaching": "none", - "httpCacheDirectory": "httpCacheDirectory", - "httpMaxCacheEntries": 1, - "httpMaxCacheEntrySize": 2 - } -}""" + 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(_) 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 b497a711f..8f38882d5 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 @@ -30,7 +30,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetada class FileBackedHttpMetadataResolverRepositoryTests extends Specification { @Autowired - FileBackedHttpMetadataResolverRepository repositoryUnderTest + MetadataResolverRepository repositoryUnderTest @Autowired EntityManager entityManager @@ -97,7 +97,7 @@ class FileBackedHttpMetadataResolverRepositoryTests extends Specification { "httpMetadataResolverAttributes": { "httpClientRef": "httpClientRef", "connectionRequestTimeout": "connectionRequestTimeout", - "requestTimeout": "requestTimeout", + "connectionTimeout": "connectionTimeout", "socketTimeout": "socketTimeout", "disregardTLSCertificate": true, "tlsTrustEngineRef": "tlsTrustEngineRef", @@ -119,9 +119,9 @@ class FileBackedHttpMetadataResolverRepositoryTests extends Specification { entityManager.flush() then: - def item1 = repositoryUnderTest.findByResourceId(persistedResolver.resourceId) + def item1 = repositoryUnderTest.findByName(persistedResolver.name) entityManager.clear() - def item2 = repositoryUnderTest.findByResourceId(persistedResolver.resourceId) + 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 890b79ed5..7860d70b3 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 @@ -3,13 +3,15 @@ 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.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget + 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 -import edu.internet2.tier.shibboleth.admin.util.AttributeUtility + import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -38,8 +40,6 @@ class MetadataResolverRepositoryTest extends Specification { @Autowired OpenSamlObjects openSamlObjects - AttributeUtility attributeUtility - def service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) def "test persisting a metadata 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 86f923b2a..7bf8c2cba 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 @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFil import edu.internet2.tier.shibboleth.admin.ui.domain.filters.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 org.opensaml.saml.metadata.resolver.ChainingMetadataResolver import org.opensaml.saml.metadata.resolver.MetadataResolver @@ -82,6 +83,9 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { @Autowired MetadataResolverRepository metadataResolverRepository + @Autowired + AttributeUtility attributeUtility + @Bean MetadataResolver metadataResolver() { def resolver = new ChainingMetadataResolver().with { @@ -97,8 +101,10 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { } if (!metadataResolverRepository.findAll().iterator().hasNext()) { - edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.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 b9623e4dc..c3dadb8c2 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 @@ -6,6 +6,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFil import edu.internet2.tier.shibboleth.admin.ui.domain.filters.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 @@ -32,7 +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.* +import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedXmlIsTheSameAsExpectedXml + @SpringBootTest @DataJpaTest @@ -72,6 +74,8 @@ class JPAMetadataResolverServiceImplTests extends Specification { domBuilder = DOMBuilder.newInstance() writer = new StringWriter() markupBuilder = new MarkupBuilder(writer) + markupBuilder.omitNullAttributes = true + markupBuilder.omitEmptyAttributes = true } def cleanup() { @@ -131,15 +135,28 @@ class JPAMetadataResolverServiceImplTests extends Specification { def filter = testObjectGenerator.entityRoleWhitelistFilter() when: - genXmlSnippet(markupBuilder) { JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeFor(filter, it) } + genXmlSnippet(markupBuilder) { JPAMetadataResolverServiceImpl.cast(metadataResolverService).constructXmlNodeForFilter(filter, it) } then: assert generatedXmlIsTheSameAsExpectedXml('/conf/533.xml', domBuilder.parseText(writer.toString())) } + 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', + 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' 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 fd7f59065..0212a788f 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 @@ -27,6 +27,7 @@ class TestHelpers { } static generatedXmlIsTheSameAsExpectedXml(String expectedXmlResource, Document generatedXml) { - !DiffBuilder.compare(Input.fromStream(this.class.getResourceAsStream(expectedXmlResource))).withTest(Input.fromDocument(generatedXml)).ignoreComments().ignoreWhitespace().build().hasDifferences() + !DiffBuilder.compare(Input.fromStream(TestHelpers.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 fce929b75..4027d8739 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 @@ -14,6 +14,9 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter 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 @@ -33,6 +36,29 @@ 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 buildAllTypesOfFilterList() { List filterList = new ArrayList<>() (1..generator.randomInt(4, 10)).each { @@ -220,6 +246,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 @@ + + + + + diff --git a/gradle.properties b/gradle.properties index ea90b9954..a78a75682 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,4 +10,6 @@ spring-boot.version=2.0.0.RELEASE hibernate.version=5.2.11.Final -lucene.version=7.2.1 \ No newline at end of file +lucene.version=7.2.1 + +org.gradle.jvmargs=-Xmx4g XX:-UseGCOverheadLimit diff --git a/ui/src/app/domain/component/i18n-text.component.html b/ui/src/app/domain/component/i18n-text.component.html index 80b821a9f..7484dd61a 100644 --- a/ui/src/app/domain/component/i18n-text.component.html +++ b/ui/src/app/domain/component/i18n-text.component.html @@ -15,6 +15,11 @@ Signing Encryption Both + + SP SSO Descriptor Information + Organization Information + User Interface / MDUI Information + Security Descriptor Information Entity ID Service Provider Name diff --git a/ui/src/app/metadata-provider/action/copy.action.ts b/ui/src/app/metadata-provider/action/copy.action.ts index 198a976db..1343aa9d1 100644 --- a/ui/src/app/metadata-provider/action/copy.action.ts +++ b/ui/src/app/metadata-provider/action/copy.action.ts @@ -8,6 +8,8 @@ export enum CopySourceActionTypes { UPDATE_PROVIDER_COPY = '[Copy Provider] Update Provider Copy Request', + UPDATE_PROVIDER_COPY_SECTIONS = '[Copy Provider] Update Provider Sections', + SAVE_PROVIDER_COPY_REQUEST = '[Copy Provider] Save Provider Copy Request', SAVE_PROVIDER_COPY_SUCCESS = '[Copy Provider] Save Provider Copy Request', SAVE_PROVIDER_COPY_ERROR = '[Copy Provider] Save Provider Copy Request', @@ -37,8 +39,15 @@ export class UpdateProviderCopy implements Action { constructor(public payload: Partial) { } } +export class UpdateProviderCopySections implements Action { + readonly type = CopySourceActionTypes.UPDATE_PROVIDER_COPY_SECTIONS; + + constructor(public payload: string[]) { } +} + export type CopySourceActionUnion = | CreateProviderCopyRequest | CreateProviderCopySuccess | CreateProviderCopyError - | UpdateProviderCopy; + | UpdateProviderCopy + | UpdateProviderCopySections; diff --git a/ui/src/app/metadata-provider/container/blank-provider.component.html b/ui/src/app/metadata-provider/container/blank-provider.component.html index c5aca7057..0f37f6b5b 100644 --- a/ui/src/app/metadata-provider/container/blank-provider.component.html +++ b/ui/src/app/metadata-provider/container/blank-provider.component.html @@ -1,55 +1,59 @@ -
- +
+
+ + + + + Service Provider Name is required + + +
+
+ + + + + Entity ID is required + + + + Entity ID must be unique + +
+ - - -
-
- - - - - Service Provider Name is required - - -
-
- - - - - Entity ID is required - - - - Entity ID must be unique - -
- -
- + + +
+ + + diff --git a/ui/src/app/metadata-provider/container/copy-provider.component.html b/ui/src/app/metadata-provider/container/copy-provider.component.html index ec45620bb..1fd0f0763 100644 --- a/ui/src/app/metadata-provider/container/copy-provider.component.html +++ b/ui/src/app/metadata-provider/container/copy-provider.component.html @@ -1,75 +1,126 @@ -
- +
+
+ + + + + + + + +
+
+ + + + + Service Provider Name is required + + +
+
+ + + + + Entity ID is required + + + + Entity ID must be unique + +
+ - - -
-
- - - - - - - - -
-
- - - - - Service Provider Name is required - - -
-
- - - - - Entity ID is required - - - - Entity ID must be unique - -
- -
- \ No newline at end of file + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + +
Sections to Copy?Yes
+
+
+ + +
+
+
Check All Attributes + +
Clear All Attributes + +
+
+ diff --git a/ui/src/app/metadata-provider/container/copy-provider.component.spec.ts b/ui/src/app/metadata-provider/container/copy-provider.component.spec.ts index 54981361c..1a1b3b406 100644 --- a/ui/src/app/metadata-provider/container/copy-provider.component.spec.ts +++ b/ui/src/app/metadata-provider/container/copy-provider.component.spec.ts @@ -1,3 +1,4 @@ +import { ViewChild, Component } from '@angular/core'; import { TestBed, ComponentFixture } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -8,9 +9,21 @@ import * as fromProvider from '../reducer'; import { CopyProviderComponent } from './copy-provider.component'; import { SharedModule } from '../../shared/shared.module'; import { NavigatorService } from '../../core/service/navigator.service'; +import { I18nTextComponent } from '../../domain/component/i18n-text.component'; + +@Component({ + template: `` +}) +class TestHostComponent { + @ViewChild(CopyProviderComponent) + public formUnderTest: CopyProviderComponent; + + onSave(event: any): void {} +} describe('Copy Provider Page', () => { - let fixture: ComponentFixture; + let fixture: ComponentFixture; let store: Store; let instance: CopyProviderComponent; @@ -26,23 +39,40 @@ describe('Copy Provider Page', () => { SharedModule ], declarations: [ - CopyProviderComponent + CopyProviderComponent, + I18nTextComponent, + TestHostComponent ], providers: [ NavigatorService ] }); - fixture = TestBed.createComponent(CopyProviderComponent); - instance = fixture.componentInstance; + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance.formUnderTest; store = TestBed.get(Store); + fixture.detectChanges(); spyOn(store, 'dispatch').and.callThrough(); }); it('should compile', () => { - fixture.detectChanges(); - expect(fixture).toBeDefined(); }); + + describe('next method', () => { + it('should dispatch an action to create a copy', () => { + instance.next(); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('onChange method', () => { + it('should dispatch an action to update the selected sections to copy', () => { + instance.onChange('relyingPartyOverrides'); + expect(store.dispatch).toHaveBeenCalled(); + instance.onChange('relyingPartyOverrides'); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); }); diff --git a/ui/src/app/metadata-provider/container/copy-provider.component.ts b/ui/src/app/metadata-provider/container/copy-provider.component.ts index 6f68a6dcb..c7b59b141 100644 --- a/ui/src/app/metadata-provider/container/copy-provider.component.ts +++ b/ui/src/app/metadata-provider/container/copy-provider.component.ts @@ -5,10 +5,10 @@ import { EventEmitter } from '@angular/core'; import { FormBuilder, FormGroup, FormControl, FormControlName, Validators, AbstractControl } from '@angular/forms'; -import { Observable, Subject } from 'rxjs'; +import { Observable, Subject, of } from 'rxjs'; import { Store } from '@ngrx/store'; -import { startWith, take } from 'rxjs/operators'; +import { startWith, take, last } from 'rxjs/operators'; import { AddDraftRequest } from '../../domain/action/draft-collection.action'; import { AddProviderRequest, UploadProviderRequest } from '../../domain/action/provider-collection.action'; @@ -17,7 +17,7 @@ import { EntityValidators } from '../../domain/service/entity-validators.service import { SearchIds } from '../action/search.action'; import * as fromProvider from '../reducer'; import { Provider } from '../../domain/entity/provider'; -import { CreateProviderCopyRequest } from '../action/copy.action'; +import { CreateProviderCopyRequest, UpdateProviderCopySections} from '../action/copy.action'; @Component({ @@ -30,6 +30,22 @@ export class CopyProviderComponent implements OnInit { providerForm: FormGroup; ids$: Observable; searchResults$: Observable; + selected$: Observable; + selected: string[]; + + sections = [ + { i18nKey: 'organizationInfo', property: 'organization' }, + { i18nKey: 'contacts', property: 'contacts' }, + { i18nKey: 'uiMduiInfo', property: 'mdui' }, + { i18nKey: 'spSsoDescriptorInfo', property: 'serviceProviderSsoDescriptor' }, + { i18nKey: 'logoutEndpoints', property: 'logoutEndpoints' }, + { i18nKey: 'securityDescriptorInfo', property: 'securityInfo' }, + { i18nKey: 'assertionConsumerServices', property: 'assertionConsumerServices' }, + { i18nKey: 'relyingPartyOverrides', property: 'relyingPartyOverrides' }, + { i18nKey: 'attributeRelease', property: 'attributeRelease' } + ]; + + sections$ = of(this.sections); constructor( private store: Store, @@ -37,13 +53,16 @@ export class CopyProviderComponent implements OnInit { ) { this.ids$ = this.store.select(fromCollections.getAllEntityIds); this.searchResults$ = this.store.select(fromProvider.getSearchResults); + this.selected$ = this.store.select(fromProvider.getSectionsToCopy); + + this.selected$.subscribe(selected => this.selected = selected); } ngOnInit(): void { this.providerForm = this.fb.group({ serviceProviderName: ['', [Validators.required]], entityId: ['', Validators.required, EntityValidators.createUniqueIdValidator(this.ids$)], - target: ['', [Validators.required], [EntityValidators.existsInCollection(this.ids$)]] + target: ['', [Validators.required], [EntityValidators.existsInCollection(this.ids$)]], }); this.store.select(fromProvider.getAttributes) @@ -64,5 +83,18 @@ export class CopyProviderComponent implements OnInit { })); } - updateOptions(query: string): void {} -} /* istanbul ignore next */ + onChange(attr: string): void { + this.store.dispatch( + new UpdateProviderCopySections( + this.selected.indexOf(attr) > -1 ? this.selected.filter(a => a !== attr) : [...this.selected, attr] + ) + ); + } + + onCheckAll(): void { + this.store.dispatch(new UpdateProviderCopySections(this.sections.map(section => section.property))); + } + onCheckNone(event: Event | null = null): void { + this.store.dispatch(new UpdateProviderCopySections([])); + } +} diff --git a/ui/src/app/metadata-provider/container/new-provider.component.html b/ui/src/app/metadata-provider/container/new-provider.component.html index 4ba004908..37d373546 100644 --- a/ui/src/app/metadata-provider/container/new-provider.component.html +++ b/ui/src/app/metadata-provider/container/new-provider.component.html @@ -54,11 +54,9 @@

How are you addi
-
- -
+ diff --git a/ui/src/app/metadata-provider/container/new-provider.component.spec.ts b/ui/src/app/metadata-provider/container/new-provider.component.spec.ts index e81de97f1..4c320fa21 100644 --- a/ui/src/app/metadata-provider/container/new-provider.component.spec.ts +++ b/ui/src/app/metadata-provider/container/new-provider.component.spec.ts @@ -16,6 +16,7 @@ import * as fromProvider from '../reducer'; import * as fromCollections from '../../domain/reducer'; import { RouterStub } from '../../../testing/router.stub'; import { ActivatedRouteStub } from '../../../testing/activated-route.stub'; +import { I18nTextComponent } from '../../domain/component/i18n-text.component'; describe('New Provider Page', () => { let fixture: ComponentFixture; @@ -40,7 +41,8 @@ describe('New Provider Page', () => { NewProviderComponent, BlankProviderComponent, UploadProviderComponent, - CopyProviderComponent + CopyProviderComponent, + I18nTextComponent ], providers: [ NavigatorService, diff --git a/ui/src/app/metadata-provider/container/upload-provider.component.html b/ui/src/app/metadata-provider/container/upload-provider.component.html index 86c2b0a3a..3b00d5e1a 100644 --- a/ui/src/app/metadata-provider/container/upload-provider.component.html +++ b/ui/src/app/metadata-provider/container/upload-provider.component.html @@ -1,59 +1,63 @@ -
- -
-
- - - - - Service Provider Name is required - - -
-
- -
- - -
-
-
- — - OR - — -
-
- - -
- -
-
+
+
+
+ +
+
+ + + + + Service Provider Name is required + + +
+
+ +
+ + +
+
+
+ — + OR + — +
+
+ + +
+ +
+
+
+
diff --git a/ui/src/app/metadata-provider/effect/copy.effect.ts b/ui/src/app/metadata-provider/effect/copy.effect.ts index 425657ece..f3b5473d1 100644 --- a/ui/src/app/metadata-provider/effect/copy.effect.ts +++ b/ui/src/app/metadata-provider/effect/copy.effect.ts @@ -25,17 +25,19 @@ export class CopyProviderEffects { copyRequest$ = this.actions$.pipe( ofType(CopySourceActionTypes.CREATE_PROVIDER_COPY_REQUEST), map(action => action.payload), - withLatestFrom(this.store.select(fromCollection.getProviderCollection)), - switchMap(([attrs, providers]) => { + withLatestFrom( + this.store.select(fromCollection.getProviderCollection), + this.store.select(fromProvider.getSectionsToCopy) + ), + switchMap(([attrs, providers, sections]) => { const { serviceProviderName, entityId } = attrs; const provider = providers.find(p => p.entityId === attrs.target); - const { attributeRelease, relyingPartyOverrides } = provider; + const copied = sections.reduce((c, section) => ({ ...c, ...{[section]: provider[section] } }), {}); const action = provider ? new CreateProviderCopySuccess(new Provider({ serviceProviderName, entityId, - attributeRelease, - relyingPartyOverrides + ...copied })) : new CreateProviderCopyError(new Error('Not found')); return of(action); diff --git a/ui/src/app/metadata-provider/reducer/copy.reducer.spec.ts b/ui/src/app/metadata-provider/reducer/copy.reducer.spec.ts index e02339f84..34b69b258 100644 --- a/ui/src/app/metadata-provider/reducer/copy.reducer.spec.ts +++ b/ui/src/app/metadata-provider/reducer/copy.reducer.spec.ts @@ -17,13 +17,7 @@ describe('Provider -> Copy Reducer', () => { describe(`${CopySourceActionTypes.CREATE_PROVIDER_COPY_REQUEST} action`, () => { it('should set properties on the state', () => { - const obj = { - target: null, - serviceProviderName: null, - entityId: null, - provider: null, - saving: false - }; + const obj = { ...snapshot }; const result = reducer(snapshot, new CreateProviderCopyRequest(obj)); expect(result).toEqual(obj); @@ -33,13 +27,7 @@ describe('Provider -> Copy Reducer', () => { describe(`${CopySourceActionTypes.CREATE_PROVIDER_COPY_SUCCESS} action`, () => { it('should set properties on the state', () => { const p = new Provider({}); - const obj = { - target: null, - serviceProviderName: null, - entityId: null, - provider: null, - saving: false - }; + const obj = { ...snapshot }; const result = reducer(snapshot, new actions.CreateProviderCopySuccess(p)); expect(result.provider).toBe(p); @@ -49,13 +37,7 @@ describe('Provider -> Copy Reducer', () => { describe(`${CopySourceActionTypes.CREATE_PROVIDER_COPY_ERROR} action`, () => { it('should set properties on the state', () => { const p = new Provider({}); - const obj = { - target: null, - serviceProviderName: null, - entityId: null, - provider: null, - saving: false - }; + const obj = { ...snapshot }; const result = reducer(snapshot, new actions.CreateProviderCopyError(new Error())); expect(result.provider).toBeNull(); @@ -64,13 +46,7 @@ describe('Provider -> Copy Reducer', () => { describe(`${CopySourceActionTypes.UPDATE_PROVIDER_COPY} action`, () => { it('should set properties on the state', () => { - const obj = { - target: null, - serviceProviderName: null, - entityId: null, - provider: new Provider({}), - saving: false - }; + const obj = { ...snapshot, provider: new Provider({}) }; const result = reducer(snapshot, new actions.UpdateProviderCopy({id: 'foo'})); expect(result.provider.id).toBe('foo'); @@ -80,13 +56,7 @@ describe('Provider -> Copy Reducer', () => { describe(`${ fromCollection.ProviderCollectionActionTypes.ADD_PROVIDER } action`, () => { it('should set properties on the state', () => { const p = new Provider({}); - const obj = { - target: null, - serviceProviderName: null, - entityId: null, - provider: p, - saving: false - }; + const obj = { ...snapshot, provider: p }; const result = reducer(snapshot, new fromCollection.AddProviderRequest(p)); expect(result.saving).toBe(true); @@ -96,13 +66,7 @@ describe('Provider -> Copy Reducer', () => { describe(`${fromCollection.ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS} action`, () => { it('should set properties on the state', () => { const p = new Provider({}); - const obj = { - target: null, - serviceProviderName: null, - entityId: null, - provider: p, - saving: false - }; + const obj = { ...snapshot, provider: p }; const result = reducer(snapshot, new fromCollection.AddProviderSuccess(p)); expect(result.saving).toBe(false); @@ -112,13 +76,7 @@ describe('Provider -> Copy Reducer', () => { describe(`${fromCollection.ProviderCollectionActionTypes.ADD_PROVIDER_FAIL} action`, () => { it('should set properties on the state', () => { const p = new Provider({}); - const obj = { - target: null, - serviceProviderName: null, - entityId: null, - provider: p, - saving: false - }; + const obj = { ...snapshot, provider: p }; const result = reducer(snapshot, new fromCollection.AddProviderFail(p)); expect(result.saving).toBe(false); diff --git a/ui/src/app/metadata-provider/reducer/copy.reducer.ts b/ui/src/app/metadata-provider/reducer/copy.reducer.ts index 88ce7aaf4..416086eba 100644 --- a/ui/src/app/metadata-provider/reducer/copy.reducer.ts +++ b/ui/src/app/metadata-provider/reducer/copy.reducer.ts @@ -10,6 +10,7 @@ export interface CopyState { entityId: string; provider: MetadataProvider; saving: boolean; + sections: string[]; } export const initialState: CopyState = { @@ -17,11 +18,20 @@ export const initialState: CopyState = { serviceProviderName: null, entityId: null, provider: null, - saving: false + saving: false, + sections: [] }; export function reducer(state = initialState, action: CopySourceActionUnion | ProviderCollectionActionsUnion): CopyState { switch (action.type) { + case CopySourceActionTypes.UPDATE_PROVIDER_COPY_SECTIONS: { + return { + ...state, + sections: [ + ...action.payload + ] + }; + } case CopySourceActionTypes.CREATE_PROVIDER_COPY_REQUEST: { return { ...state, @@ -70,4 +80,5 @@ export const getCopyAttributes = (state: CopyState) => ({ serviceProviderName: state.serviceProviderName, target: state.target }); +export const getCopySections = (state: CopyState) => state.sections; export const getSaving = (state: CopyState) => state.saving; diff --git a/ui/src/app/metadata-provider/reducer/index.ts b/ui/src/app/metadata-provider/reducer/index.ts index 7e6ec4d65..d1981f19a 100644 --- a/ui/src/app/metadata-provider/reducer/index.ts +++ b/ui/src/app/metadata-provider/reducer/index.ts @@ -26,6 +26,7 @@ export const getCopyFromState = createSelector(getProviderState, getCopyFromStat export const getCopy = createSelector(getCopyFromState, fromCopy.getCopy); export const getSaving = createSelector(getCopyFromState, fromCopy.getSaving); export const getAttributes = createSelector(getCopyFromState, fromCopy.getCopyAttributes); +export const getSectionsToCopy = createSelector(getCopyFromState, fromCopy.getCopySections); export const getSearchFromState = createSelector(getProviderState, getSearchFromStateFn); export const getSearchResults = createSelector(getSearchFromState, fromSearch.getMatches);