diff --git a/backend/src/integration/resources/SHIBUI-1392.side b/backend/src/integration/resources/SHIBUI-1392.side index 20cb9c8ee..ec421765b 100644 --- a/backend/src/integration/resources/SHIBUI-1392.side +++ b/backend/src/integration/resources/SHIBUI-1392.side @@ -389,20 +389,13 @@ ["xpath=//p", "xpath:position"] ], "value": "" - }, { - "id": "84be6a98-5739-42e8-b7ca-06a6c86e9f40", - "comment": "", - "command": "editContent", - "target": "id=/nameIdFormatFilterTarget.target", - "targets": [], - "value": "(true);" }, { "id": "05870356-d3db-4540-bb3f-db34b1cf65f1", "comment": "", "command": "sendKeys", "target": "id=/nameIdFormatFilterTarget.target", "targets": [], - "value": "eval" + "value": "eval(true);" }, { "id": "d7721254-68c9-4140-af2a-1757cce99da7", "comment": "", diff --git a/backend/src/integration/resources/SHIBUI-1407-1.side b/backend/src/integration/resources/SHIBUI-1407-1.side index 374834ff5..5960f7fc1 100644 --- a/backend/src/integration/resources/SHIBUI-1407-1.side +++ b/backend/src/integration/resources/SHIBUI-1407-1.side @@ -2187,6 +2187,13 @@ "target": "5000", "targets": [], "value": "" + }, { + "id": "c2fcb197-7e0d-4b64-82a5-ad24cf24126b", + "comment": "", + "command": "waitForElementEditable", + "target": "id=/serviceProviderName", + "targets": [], + "value": "30000" }, { "id": "99731068-2016-4a7f-8a38-febfb711d027", "comment": "", diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/EntityDescriptorSchemaValidatingControllerAdvice.groovy similarity index 70% rename from backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy rename to backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/EntityDescriptorSchemaValidatingControllerAdvice.groovy index 620d2252c..f25950256 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/EntityDescriptorSchemaValidatingControllerAdvice.groovy @@ -2,7 +2,6 @@ package edu.internet2.tier.shibboleth.admin.ui.jsonschema import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation -import mjson.Json import org.springframework.beans.factory.annotation.Autowired import org.springframework.core.MethodParameter import org.springframework.http.HttpInputMessage @@ -14,6 +13,7 @@ import javax.annotation.PostConstruct import java.lang.reflect.Type import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.metadataSourcesSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator.validatePayloadAgainstSchema /** * Controller advice implementation for validating relying party overrides payload coming from UI layer @@ -22,7 +22,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocati * @author Dmitriy Kopylenko */ @ControllerAdvice -class RelyingPartyOverridesJsonSchemaValidatingControllerAdvice extends RequestBodyAdviceAdapter { +class EntityDescriptorSchemaValidatingControllerAdvice extends RequestBodyAdviceAdapter { @Autowired JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry @@ -38,22 +38,12 @@ class RelyingPartyOverridesJsonSchemaValidatingControllerAdvice extends RequestB HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) throws IOException { - def bytes = inputMessage.body.bytes - def schema = Json.schema(this.jsonSchemaLocation.uri) - - def stream = new ByteArrayInputStream(bytes) - def validationResult = schema.validate(Json.read(stream.getText())) - if (!validationResult.at('ok')) { - throw new JsonSchemaValidationFailedException(validationResult.at('errors').asList()) - } - return [ - getBody: { new ByteArrayInputStream(bytes) }, - getHeaders: { inputMessage.headers } - ] as HttpInputMessage + + return validatePayloadAgainstSchema(inputMessage, this.jsonSchemaLocation.uri) } @PostConstruct void init() { - this.jsonSchemaLocation = metadataSourcesSchema(this.jsonSchemaResourceLocationRegistry); + this.jsonSchemaLocation = metadataSourcesSchema(this.jsonSchemaResourceLocationRegistry) } } diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/LowLevelJsonSchemaValidator.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/LowLevelJsonSchemaValidator.groovy new file mode 100644 index 000000000..bcf25b4d9 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/LowLevelJsonSchemaValidator.groovy @@ -0,0 +1,90 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema + +import mjson.Json +import org.springframework.http.HttpInputMessage + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.dynamicHttpMetadataProviderSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.entityAttributesFiltersSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.filesystemMetadataProviderSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.localDynamicMetadataProviderSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.nameIdFormatFilterSchema + +/** + * Currently uses mjson library. + * + * @author Dmitriy Kopylenko + */ +class LowLevelJsonSchemaValidator { + + static HttpInputMessage validatePayloadAgainstSchema(HttpInputMessage inputMessage, URI schemaUri) { + def origInput = [inputMessage.body.bytes, inputMessage.headers] + def json = extractJsonPayload(origInput) + def schema = Json.schema(schemaUri) + doValidate(origInput, schema, json) + } + + static HttpInputMessage validateMetadataResolverTypePayloadAgainstSchema(HttpInputMessage inputMessage, + JsonSchemaResourceLocationRegistry schemaRegistry) { + + def origInput = [inputMessage.body.bytes, inputMessage.headers] + def json = extractJsonPayload(origInput) + def schemaUri = null + switch (json.asMap()['@type']) { + case 'LocalDynamicMetadataResolver': + schemaUri = localDynamicMetadataProviderSchema(schemaRegistry).uri + break + case 'DynamicHttpMetadataResolver': + schemaUri = dynamicHttpMetadataProviderSchema(schemaRegistry).uri + break + case 'FilesystemMetadataResolver': + schemaUri = filesystemMetadataProviderSchema(schemaRegistry).uri + break + default: + break + } + if (!schemaUri) { + return newInputMessage(origInput) + } + doValidate(origInput, Json.schema(schemaUri), json) + } + + static HttpInputMessage validateMetadataFilterTypePayloadAgainstSchema(HttpInputMessage inputMessage, + JsonSchemaResourceLocationRegistry schemaRegistry) { + def origInput = [inputMessage.body.bytes, inputMessage.headers] + def json = extractJsonPayload(origInput) + def schemaUri = null + switch (json.asMap()['@type']) { + case 'EntityAttributes': + schemaUri = entityAttributesFiltersSchema(schemaRegistry).uri + break + case 'NameIDFormat': + schemaUri = nameIdFormatFilterSchema(schemaRegistry).uri + break + default: + break + } + if (!schemaUri) { + return newInputMessage(origInput) + } + doValidate(origInput, Json.schema(schemaUri), json) + } + + private static Json extractJsonPayload(List origInput) { + Json.read(new ByteArrayInputStream(origInput[0]).getText()) + } + + private static HttpInputMessage doValidate(List origInput, Json.Schema schema, Json json) { + def validationResult = schema.validate(json) + if (!validationResult.at('ok')) { + throw new JsonSchemaValidationFailedException(validationResult.at('errors').asList()) + } + newInputMessage(origInput) + } + + private static newInputMessage(origInput) { + [ + getBody : { new ByteArrayInputStream(origInput[0]) }, + getHeaders: { origInput[1] } + ] as HttpInputMessage + } +} diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataFiltersSchemaValidatingControllerAdvice.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataFiltersSchemaValidatingControllerAdvice.groovy new file mode 100644 index 000000000..3608da194 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataFiltersSchemaValidatingControllerAdvice.groovy @@ -0,0 +1,40 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema + +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.MethodParameter +import org.springframework.http.HttpInputMessage +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter + +import java.lang.reflect.Type + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator.validateMetadataFilterTypePayloadAgainstSchema + +/** + * Controller advice implementation for validating metadata filters payload coming from UI layer + * against pre-defined JSON schema for their respected types. Choosing of the appropriate schema based on incoming + * resolver types is delegated to @{LowLevelJsonSchemaValidator}. + * + * @author Dmitriy Kopylenko + */ +@ControllerAdvice +class MetadataFiltersSchemaValidatingControllerAdvice extends RequestBodyAdviceAdapter { + + @Autowired + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry + + @Override + boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { + targetType.typeName == MetadataFilter.typeName + } + + @Override + HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) + throws IOException { + + validateMetadataFilterTypePayloadAgainstSchema(inputMessage, jsonSchemaResourceLocationRegistry) + } +} diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataResolversSchemaValidatingControllerAdvice.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataResolversSchemaValidatingControllerAdvice.groovy new file mode 100644 index 000000000..1c051a9c0 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataResolversSchemaValidatingControllerAdvice.groovy @@ -0,0 +1,41 @@ +package edu.internet2.tier.shibboleth.admin.ui.jsonschema + + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.core.MethodParameter +import org.springframework.http.HttpInputMessage +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter + +import java.lang.reflect.Type + +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.LowLevelJsonSchemaValidator.validateMetadataResolverTypePayloadAgainstSchema + +/** + * Controller advice implementation for validating metadata resolvers payload coming from UI layer + * against pre-defined JSON schema for their respected types. Choosing of the appropriate schema based on incoming + * resolver types is delegated to @{LowLevelJsonSchemaValidator}. + * + * @author Dmitriy Kopylenko + */ +@ControllerAdvice +class MetadataResolversSchemaValidatingControllerAdvice extends RequestBodyAdviceAdapter { + + @Autowired + JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry + + @Override + boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { + targetType.typeName == MetadataResolver.typeName + } + + @Override + HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class> converterType) + throws IOException { + + validateMetadataResolverTypePayloadAgainstSchema(inputMessage, jsonSchemaResourceLocationRegistry) + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicMetadataResolverAttributes.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicMetadataResolverAttributes.java index 564f2871e..d817284ba 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicMetadataResolverAttributes.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/DynamicMetadataResolverAttributes.java @@ -20,7 +20,7 @@ public class DynamicMetadataResolverAttributes { private String taskTimerRef; - private Double refreshDelayFactor = 0.75; + private Float refreshDelayFactor = 0.75F; private String minCacheDuration = "PT10M"; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java index d52e47448..862624b05 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/opensaml/OpenSamlMetadataResolverConstructorHelper.java @@ -57,7 +57,7 @@ public static void updateOpenSamlMetadataResolverFromDynamicMetadataResolverAttr } if (attributes.getRefreshDelayFactor() != null) { - dynamicMetadataResolver.setRefreshDelayFactor(attributes.getRefreshDelayFactor().floatValue()); + dynamicMetadataResolver.setRefreshDelayFactor(attributes.getRefreshDelayFactor()); } if (attributes.getRemoveIdleEntityData() != null) { diff --git a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json index f7106c6e5..28758fee6 100644 --- a/backend/src/main/resources/dynamic-http-metadata-provider.schema.json +++ b/backend/src/main/resources/dynamic-http-metadata-provider.schema.json @@ -4,7 +4,6 @@ "name", "@type", "xmlId", - "metadataURL", "metadataRequestURLConstructionScheme" ], "properties": { @@ -147,14 +146,15 @@ "refreshDelayFactor": { "title": "label.refresh-delay-factor", "description": "tooltip.refresh-delay-factor", - "type": "string", + "type": "number", "widget": { - "id": "string", - "help": "message.real-number" + "id": "float", + "help": "message.real-number", + "step": 0.01 }, "placeholder": "label.real-number", - "default": "", - "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" + "minimum": 0.001, + "maximum": 0.999 }, "minCacheDuration": { "title": "label.min-cache-duration", @@ -576,11 +576,14 @@ } }, "metadataFilters": { + "$id": "metadataFilters", "title": "", "description": "", - "type": "object", - "properties": { - "RequiredValidUntil": { + "type": "array", + "additionalItems": true, + "items": [ + { + "$id": "RequiredValidUntil", "title": "label.required-valid-until", "type": "object", "widget": { @@ -611,7 +614,8 @@ } } }, - "SignatureValidation": { + { + "$id": "SignatureValidation", "title": "label.signature-validation-filter", "type": "object", "widget": { @@ -654,7 +658,8 @@ } ] }, - "EntityRoleWhiteList": { + { + "$id": "EntityRoleWhiteList", "title": "label.entity-role-whitelist", "type": "object", "widget": { @@ -700,7 +705,7 @@ } } } - } + ] } } -} \ No newline at end of file +} diff --git a/backend/src/main/resources/file-system-metadata-provider.schema.json b/backend/src/main/resources/file-system-metadata-provider.schema.json index af2f8af0a..f6037c79b 100644 --- a/backend/src/main/resources/file-system-metadata-provider.schema.json +++ b/backend/src/main/resources/file-system-metadata-provider.schema.json @@ -128,14 +128,15 @@ "refreshDelayFactor": { "title": "label.refresh-delay-factor", "description": "tooltip.refresh-delay-factor", - "type": "string", + "type": "number", "widget": { - "id": "string", - "help": "message.real-number" + "id": "float", + "help": "message.real-number", + "step": 0.01 }, "placeholder": "label.real-number", - "default": "", - "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" + "minimum": 0.001, + "maximum": 0.999 } } } diff --git a/backend/src/main/resources/filebacked-http-metadata-provider.schema.json b/backend/src/main/resources/filebacked-http-metadata-provider.schema.json new file mode 100644 index 000000000..505fe6ebd --- /dev/null +++ b/backend/src/main/resources/filebacked-http-metadata-provider.schema.json @@ -0,0 +1,653 @@ +{ + "type": "object", + "order": [ + "name", + "@type", + "enabled", + "xmlId", + "metadataURL", + "initializeFromBackupFile", + "backingFile", + "backupFileInitNextRefreshDelay", + "requireValidMetadata", + "failFastInitialization", + "useDefaultPredicateRegistry", + "satisfyAnyPredicates", + "httpMetadataResolverAttributes", + "reloadableMetadataResolverAttributes", + "metadataFilters" + ], + "required": [ + "name", + "@type", + "xmlId", + "metadataURL", + "backingFile", + "backupFileInitNextRefreshDelay" + ], + "properties": { + "name": { + "title": "label.metadata-provider-name-dashboard-display-only", + "description": "tooltip.metadata-provider-name-dashboard-display-only", + "type": "string", + "widget": { + "id": "string", + "help": "message.must-be-unique" + } + }, + "@type": { + "title": "label.metadata-provider-type", + "description": "tooltip.metadata-provider-type", + "placeholder": "label.select-metadata-type", + "type": "string", + "readOnly": true, + "widget": { + "id": "select", + "disabled": true + }, + "oneOf": [ + { + "enum": [ + "FileBackedHttpMetadataResolver" + ], + "description": "value.file-backed-http-metadata-provider" + } + ] + }, + "enabled": { + "title": "label.enable-service", + "description": "tooltip.enable-service", + "type": "boolean", + "default": false + }, + "xmlId": { + "title": "label.xml-id", + "description": "tooltip.xml-id", + "type": "string", + "default": "", + "minLength": 1 + }, + "metadataURL": { + "title": "label.metadata-url", + "description": "tooltip.metadata-url", + "type": "string", + "default": "", + "minLength": 1 + }, + "initializeFromBackupFile": { + "title": "label.init-from-backup", + "description": "tooltip.init-from-backup", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "backingFile": { + "title": "label.backing-file", + "description": "tooltip.backing-file", + "type": "string", + "default": "" + }, + "backupFileInitNextRefreshDelay": { + "title": "label.backup-file-init-refresh-delay", + "description": "tooltip.backup-file-init-refresh-delay", + "type": "string", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "requireValidMetadata": { + "title": "label.require-valid-metadata", + "description": "tooltip.require-valid-metadata", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "failFastInitialization": { + "title": "label.fail-fast-init", + "description": "tooltip.fail-fast-init", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "useDefaultPredicateRegistry": { + "title": "label.use-default-predicate-reg", + "description": "tooltip.use-default-predicate-reg", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": true + }, + "satisfyAnyPredicates": { + "$id": "satisfyAnyPredicates", + "title": "label.satisfy-any-predicates", + "description": "tooltip.satisfy-any-predicates", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "value.true" + }, + { + "enum": [ + false + ], + "description": "value.false" + } + ], + "default": false + }, + "httpMetadataResolverAttributes": { + "$id": "httpMetadataResolverAttributes", + "order": [], + "type": "object", + "fieldsets": [ + { + "title": "label.http-connection-attributes", + "type": "section", + "fields": [ + "connectionRequestTimeout", + "connectionTimeout", + "socketTimeout" + ] + }, + { + "title": "label.http-security-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "disregardTLSCertificate" + ] + }, + { + "title": "label.http-proxy-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "proxyHost", + "proxyPort", + "proxyUser", + "proxyPassword" + ] + }, + { + "title": "label.http-caching-attributes", + "type": "section", + "class": "col-12", + "fields": [ + "httpCaching", + "httpCacheDirectory", + "httpMaxCacheEntries", + "httpMaxCacheEntrySize" + ] + }, + { + "title": "", + "type": "hidden", + "class": "col-12", + "fields": [ + "tlsTrustEngineRef", + "httpClientSecurityParametersRef", + "httpClientRef" + ] + } + ], + "properties": { + "httpClientRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "connectionRequestTimeout": { + "type": "string", + "title": "label.connection-request-timeout", + "description": "tooltip.connection-request-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "connectionTimeout": { + "type": "string", + "title": "label.connection-timeout", + "description": "tooltip.connection-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "socketTimeout": { + "type": "string", + "title": "label.socket-timeout", + "description": "tooltip.socket-timeout", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "disregardTLSCertificate": { + "type": "boolean", + "title": "label.disregard-tls-cert", + "description": "tooltip.disregard-tls-cert", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ] + }, + "tlsTrustEngineRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "httpClientSecurityParametersRef": { + "type": "string", + "title": "", + "description": "", + "placeholder": "", + "widget": "hidden" + }, + "proxyHost": { + "type": "string", + "title": "label.proxy-host", + "description": "tooltip.proxy-host", + "placeholder": "" + }, + "proxyPort": { + "type": "string", + "title": "label.proxy-port", + "description": "tooltip.proxy-port", + "placeholder": "" + }, + "proxyUser": { + "type": "string", + "title": "label.proxy-user", + "description": "tooltip.proxy-user", + "placeholder": "" + }, + "proxyPassword": { + "type": "string", + "title": "label.proxy-password", + "description": "tooltip.proxy-password", + "placeholder": "" + }, + "httpCaching": { + "type": "string", + "title": "label.http-caching", + "description": "tooltip.http-caching", + "placeholder": "label.select-caching-type", + "widget": { + "id": "select" + }, + "oneOf": [ + { + "enum": [ + "none" + ], + "description": "value.none" + }, + { + "enum": [ + "file" + ], + "description": "value.file" + }, + { + "enum": [ + "memory" + ], + "description": "value.memory" + } + ] + }, + "httpCacheDirectory": { + "type": "string", + "title": "label.http-caching-directory", + "description": "tooltip.http-caching-directory", + "placeholder": "" + }, + "httpMaxCacheEntries": { + "type": "integer", + "title": "label.http-max-cache-entries", + "description": "tooltip.http-max-cache-entries", + "placeholder": "", + "minimum": 0 + }, + "httpMaxCacheEntrySize": { + "type": "integer", + "title": "label.max-cache-entry-size", + "description": "tooltip.max-cache-entry-size", + "placeholder": "", + "minimum": 0 + } + } + }, + "reloadableMetadataResolverAttributes": { + "$id": "reloadableMetadataResolverAttributes", + "type": "object", + "properties": { + "minRefreshDelay": { + "title": "label.min-refresh-delay", + "description": "tooltip.min-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "maxRefreshDelay": { + "title": "label.max-refresh-delay", + "description": "tooltip.max-refresh-delay", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + }, + "refreshDelayFactor": { + "title": "label.refresh-delay-factor", + "description": "tooltip.refresh-delay-factor", + "type": "number", + "widget": { + "id": "float", + "help": "message.real-number", + "step": 0.01 + }, + "placeholder": "label.real-number", + "minimum": 0.001, + "maximum": 0.999 + } + } + }, + "metadataFilters": { + "$id": "metadataFilters", + "title": "", + "description": "", + "type": "array", + "additionalItems": true, + "items": [ + { + "$id": "RequiredValidUntil", + "title": "label.required-valid-until", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "maxValidityInterval": { + "title": "label.max-validity-interval", + "description": "tooltip.max-validity-interval", + "type": "string", + "placeholder": "label.duration", + "widget": { + "id": "datalist", + "data": [ + "PT0S", + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" + ] + }, + "default": null, + "pattern": "^(R\\d*\\/)?P(?:\\d+(?:\\.\\d+)?Y)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?W)?(?:\\d+(?:\\.\\d+)?D)?(?:T(?:\\d+(?:\\.\\d+)?H)?(?:\\d+(?:\\.\\d+)?M)?(?:\\d+(?:\\.\\d+)?S)?)?$" + } + } + }, + { + "$id": "SignatureValidation", + "title": "label.signature-validation-filter", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "requireSignedRoot": { + "title": "label.require-signed-root", + "description": "tooltip.require-signed-root", + "type": "boolean", + "default": true + }, + "certificateFile": { + "title": "label.certificate-file", + "description": "tooltip.certificate-file", + "type": "string" + } + }, + "anyOf": [ + { + "properties": { + "requireSignedRoot": { + "enum": [ + true + ] + } + }, + "required": [ + "certificateFile" + ] + }, + { + "properties": { + "requireSignedRoot": { + "enum": [ + false + ] + } + } + } + ] + }, + { + "$id": "EntityRoleWhiteList", + "title": "label.entity-role-whitelist", + "type": "object", + "widget": { + "id": "fieldset" + }, + "properties": { + "retainedRoles": { + "title": "label.retained-roles", + "description": "tooltip.retained-roles", + "type": "array", + "items": { + "widget": { + "id": "select" + }, + "type": "string", + "oneOf": [ + { + "enum": [ + "SPSSODescriptor" + ], + "description": "value.spdescriptor" + }, + { + "enum": [ + "AttributeAuthorityDescriptor" + ], + "description": "value.attr-auth-descriptor" + } + ] + } + }, + "removeRolelessEntityDescriptors": { + "title": "label.remove-roleless-entity-descriptors", + "description": "tooltip.remove-roleless-entity-descriptors", + "type": "boolean", + "default": true + }, + "removeEmptyEntitiesDescriptors": { + "title": "label.remove-empty-entities-descriptors", + "description": "tooltip.remove-empty-entities-descriptors", + "type": "boolean", + "default": true + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/backend/src/main/resources/local-dynamic-metadata-provider.schema.json b/backend/src/main/resources/local-dynamic-metadata-provider.schema.json index f39904f36..c7cfeb360 100644 --- a/backend/src/main/resources/local-dynamic-metadata-provider.schema.json +++ b/backend/src/main/resources/local-dynamic-metadata-provider.schema.json @@ -61,14 +61,15 @@ "refreshDelayFactor": { "title": "label.refresh-delay-factor", "description": "tooltip.refresh-delay-factor", - "type": "string", + "type": "number", "widget": { - "id": "string", - "help": "message.real-number" + "id": "float", + "help": "message.real-number", + "step": 0.01 }, "placeholder": "label.real-number", - "default": "", - "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" + "minimum": 0.001, + "maximum": 0.999 }, "minCacheDuration": { "title": "label.min-cache-duration", @@ -188,4 +189,4 @@ } } } -} \ No newline at end of file +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerSchemaValidationIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerSchemaValidationIntegrationTests.groovy new file mode 100644 index 000000000..39e630223 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerSchemaValidationIntegrationTests.groovy @@ -0,0 +1,54 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.test.context.ActiveProfiles +import spock.lang.Specification + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(["no-auth", "dev"]) +class EntityDescriptorControllerSchemaValidationIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + static RESOURCE_URI = '/api/EntityDescriptor' + + def 'POST /EntityDescriptor with invalid payload according to schema validation'() { + given: + def postedJsonBody = """ + { + "serviceProviderName": "SP", + "entityId": "ED", + "organization": null, + "serviceEnabled": true, + "createdDate": null, + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null, + "current": false + } + """ + + when: + def result = this.restTemplate.postForEntity(RESOURCE_URI, createRequestHttpEntityFor { postedJsonBody }, Map) + + then: + result.statusCodeValue == 400 + result.body.errorMessage.count('Type mistmatch for null') > 0 + } + + private static HttpEntity createRequestHttpEntityFor(Closure jsonBodySupplier) { + new HttpEntity(jsonBodySupplier(), ['Content-Type': 'application/json'] as HttpHeaders) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerSchemaValidationIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerSchemaValidationIntegrationTests.groovy new file mode 100644 index 000000000..32169b063 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerSchemaValidationIntegrationTests.groovy @@ -0,0 +1,102 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.test.context.ActiveProfiles +import spock.lang.Specification + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(["no-auth", "dev"]) +class MetadataFiltersControllerSchemaValidationIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + @Autowired + MetadataResolverRepository metadataResolverRepository + + static RESOURCE_URI = '/api/MetadataResolvers/%s/Filters' + + private HTTP_POST = { body, resourceId -> + this.restTemplate.postForEntity(resourceUriFor(RESOURCE_URI, resourceId), createRequestHttpEntityFor(body), Map) + } + + private static checkJsonValidationIsPerformed = { + assert it.statusCodeValue == 400 + assert it.body.errorMessage.count('Type mistmatch for null') > 0 + assert it.body.errorMessage.count('Type mistmatch for "not-a-boolean"') > 0 + true + } + + def 'POST for EntityAttributesFilter with invalid payload according to schema validation'() { + given: + def resolver = metadataResolverRepository.save(new DynamicHttpMetadataResolver(name: 'dmr')) + def postedJsonBody = """ + { + "name" : "EntityAttributes", + "filterEnabled" : "not-a-boolean", + "entityAttributesFilterTarget" : { + "entityAttributesFilterTargetType" : "ENTITY", + "value" : [ "CedewbJJET" ] + }, + "attributeRelease" : [ "9ktPyjjiCn" ], + "relyingPartyOverrides" : { + "signAssertion" : false, + "dontSignResponse" : true, + "turnOffEncryption" : true, + "useSha" : false, + "ignoreAuthenticationMethod" : false, + "omitNotBefore" : true, + "responderId" : null, + "nameIdFormats" : [ ], + "authenticationMethods" : [ ] + }, + "@type" : "EntityAttributes" + } + """ + + when: + def result = HTTP_POST(postedJsonBody, resolver.resourceId) + + then: + checkJsonValidationIsPerformed(result) + + } + + def 'POST for NameIdFormatFilter with invalid payload according to schema validation'() { + given: + def resolver = metadataResolverRepository.save(new FileBackedHttpMetadataResolver(name: 'fbmr', backingFile: '/tmp/metadata.xml')) + def postedJsonBody = """ + { + "name" : null, + "filterEnabled" : "not-a-boolean", + "removeExistingFormats" : false, + "formats" : [ "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" ], + "nameIdFormatFilterTarget" : { + "nameIdFormatFilterTargetType" : "ENTITY", + "value" : [ "https://sp1.example.org" ] + }, + "@type" : "NameIDFormat" + }""" + + when: + def result = HTTP_POST(postedJsonBody, resolver.resourceId) + + then: + checkJsonValidationIsPerformed(result) + } + + private static HttpEntity createRequestHttpEntityFor(String jsonBody) { + new HttpEntity(jsonBody, ['Content-Type': 'application/json'] as HttpHeaders) + } + + private static resourceUriFor(String uriTemplate, String resourceId) { + String.format(uriTemplate, resourceId) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerSchemaValidationIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerSchemaValidationIntegrationTests.groovy new file mode 100644 index 000000000..d26b90cb5 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerSchemaValidationIntegrationTests.groovy @@ -0,0 +1,151 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.test.context.ActiveProfiles +import spock.lang.Specification + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles(["no-auth", "dev"]) +class MetadataResolverControllerSchemaValidationIntegrationTests extends Specification { + + @Autowired + private TestRestTemplate restTemplate + + static RESOURCE_URI = '/api/MetadataResolvers' + + private HTTP_POST = { body -> + this.restTemplate.postForEntity(RESOURCE_URI, createRequestHttpEntityFor(body), Map) + } + + private static checkJsonValidationIsPerformed = { + assert it.statusCodeValue == 400 + assert it.body.errorMessage.count('Type mistmatch for null') > 0 + true + } + + def 'POST for LocalDynamicMetadataResolver with invalid payload according to schema validation'() { + given: + def postedJsonBody = """ + { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : null, + "requireValidMetadata" : true, + "failFastInitialization" : true, + "sortKey" : null, + "criterionPredicateRegistryRef" : null, + "useDefaultPredicateRegistry" : true, + "satisfyAnyPredicates" : false, + "metadataFilters" : [ { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityAttributes", + "filterEnabled" : false, + "version" : 463855403, + "entityAttributesFilterTarget" : { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "entityAttributesFilterTargetType" : "ENTITY", + "value" : [ "CedewbJJET" ], + "audId" : null + }, + "attributeRelease" : [ "9ktPyjjiCn" ], + "relyingPartyOverrides" : { + "signAssertion" : false, + "dontSignResponse" : true, + "turnOffEncryption" : true, + "useSha" : false, + "ignoreAuthenticationMethod" : false, + "omitNotBefore" : true, + "responderId" : null, + "nameIdFormats" : [ ], + "authenticationMethods" : [ ] + }, + "audId" : null, + "@type" : "EntityAttributes" + }, { + "createdDate" : null, + "modifiedDate" : null, + "createdBy" : null, + "modifiedBy" : null, + "name" : "EntityRoleWhiteList", + "filterEnabled" : false, + "version" : 0, + "removeRolelessEntityDescriptors" : true, + "removeEmptyEntitiesDescriptors" : true, + "retainedRoles" : [ "role1", "role2" ], + "audId" : null, + "@type" : "EntityRoleWhiteList" + } ], + "version" : 0, + "sourceDirectory" : "dir", + "sourceManagerRef" : null, + "sourceKeyGeneratorRef" : null, + "audId" : null, + "@type" : "LocalDynamicMetadataResolver" + } + """ + + when: + def result = HTTP_POST(postedJsonBody) + + then: + checkJsonValidationIsPerformed(result) + + } + + def 'POST for DynamicHttpMetadataResolver with invalid payload according to schema validation'() { + given: + def postedJsonBody = """ + { + "name" : null, + "xmlId": "123", + "metadataURL": "http://metadata", + "metadataRequestURLConstructionScheme": {"@type": "MetadataQueryProtocol", "content": "scheme"}, + "@type" : "DynamicHttpMetadataResolver" + } + """ + + when: + def result = HTTP_POST(postedJsonBody) + + then: + checkJsonValidationIsPerformed(result) + + } + + def 'POST for FilesystemMetadataResolver with invalid payload according to schema validation'() { + given: + def postedJsonBody = """ + { + "name" : null, + "xmlId": "123", + "metadataFile": "%{shib.home}/metadata.xml", + "@type" : "FilesystemMetadataResolver" + } + """ + + when: + def result = HTTP_POST(postedJsonBody) + + then: + checkJsonValidationIsPerformed(result) + + } + + private static HttpEntity createRequestHttpEntityFor(String jsonBody) { + new HttpEntity(jsonBody, ['Content-Type': 'application/json'] as HttpHeaders) + } + + +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index d22902e30..e6b0c2197 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -8,6 +8,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFil import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.LocalDynamicMetadataResolver +import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataQueryProtocolScheme import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.opensaml.OpenSamlChainingMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator @@ -27,6 +28,7 @@ import org.springframework.test.context.ActiveProfiles import spock.lang.Specification import spock.lang.Unroll +import static com.fasterxml.jackson.annotation.JsonInclude.Include.* import static org.springframework.http.HttpMethod.PUT /** @@ -59,6 +61,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { generator = new TestObjectGenerator(attributeUtility, customPropertiesConfiguration) mapper = new ObjectMapper() mapper.enable(SerializationFeature.INDENT_OUTPUT) + mapper.setSerializationInclusion(NON_NULL) mapper.registerModule(new JavaTimeModule()) } @@ -254,7 +257,13 @@ class MetadataResolversControllerIntegrationTests extends Specification { def "PUT concrete MetadataResolver with version conflict -> /api/MetadataResolvers/{resourceId}"() { given: 'One resolver is available in data store' def resolver = new DynamicHttpMetadataResolver().with { - it.name = 'Test DynamicHttpMetadataResolver' + it.name = 'DynamicHTTP' + it.xmlId = 'DynamicHTTP' + it.metadataRequestURLConstructionScheme = new MetadataQueryProtocolScheme().with { + it.transformRef = 'transformRef' + it.content = 'content' + it + } it } def resolverResourceId = resolver.resourceId diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.html b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html index e0685e743..4c57ca75a 100644 --- a/ui/src/app/metadata/configuration/component/metadata-configuration.component.html +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.html @@ -1,44 +1,46 @@ -
-
-
-
-

- - 0{{ i + 1 }} - - {{ section.label | translate }} -

-
- -
-
-
- -
- Option - - Value - {{ date | date:DATE_FORMAT }} - -
- - -
- -
- No Changes +
+ +
+
+
+

+ + 0{{ i + 1 }} + + {{ section.label | translate }} +

+
+
- +
+
+ +
+ Option + + Value + {{ date | date:DATE_FORMAT }} + +
+ + +
+
+
+
+ + -
+
\ No newline at end of file diff --git a/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts index 1b04ba636..e2d75c4da 100644 --- a/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts +++ b/ui/src/app/metadata/configuration/component/metadata-configuration.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter } from '@angular/core'; +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, OnChanges } from '@angular/core'; import { Router, ActivatedRoute } from '@angular/router'; import { MetadataConfiguration } from '../model/metadata-configuration'; import { Metadata } from '../../domain/domain.type'; @@ -6,11 +6,10 @@ import { CONFIG_DATE_FORMAT } from '../configuration.values'; @Component({ selector: 'metadata-configuration', - changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './metadata-configuration.component.html', styleUrls: ['./metadata-configuration.component.scss'] }) -export class MetadataConfigurationComponent { +export class MetadataConfigurationComponent implements OnChanges { @Input() configuration: MetadataConfiguration; @Input() definition: any; @Input() entity: Metadata; @@ -19,6 +18,8 @@ export class MetadataConfigurationComponent { @Output() preview: EventEmitter = new EventEmitter(); + zero = false; + DATE_FORMAT = CONFIG_DATE_FORMAT; constructor( @@ -26,6 +27,10 @@ export class MetadataConfigurationComponent { private activatedRoute: ActivatedRoute ) {} + ngOnChanges(): void { + this.zero = this.configuration.sections.some(s => !s.properties.length); + } + edit(id: string): void { this.router.navigate(['../', 'edit', id], { relativeTo: this.activatedRoute.parent }); } diff --git a/ui/src/app/metadata/configuration/reducer/index.ts b/ui/src/app/metadata/configuration/reducer/index.ts index 285192aff..8deb962ec 100644 --- a/ui/src/app/metadata/configuration/reducer/index.ts +++ b/ui/src/app/metadata/configuration/reducer/index.ts @@ -55,9 +55,18 @@ export const getConfigurationModelKind = createSelector(getConfigurationState, f export const getConfigurationModelId = createSelector(getConfigurationState, fromConfiguration.getModelId); export const getConfigurationDefinition = createSelector(getConfigurationState, fromConfiguration.getDefinition); -export const getConfigurationSchema = createSelector(getConfigurationState, fromConfiguration.getSchema); +export const getSchema = createSelector(getConfigurationState, fromConfiguration.getSchema); export const getConfigurationXml = createSelector(getConfigurationState, fromConfiguration.getXml); +export const processSchemaFn = (definition, schema) => { + return definition && schema ? + definition.schemaPreprocessor ? + definition.schemaPreprocessor(schema) : schema + : schema; +}; + +export const getConfigurationSchema = createSelector(getConfigurationDefinition, getSchema, processSchemaFn); + export const assignValueToProperties = (models, properties, definition: any): any[] => { return properties.map(prop => { const differences = models.some((model, index, array) => { @@ -192,8 +201,16 @@ export const getComparisonLoading = createSelector(getCompareState, fromCompare. export const getComparisonModels = createSelector(getCompareState, fromCompare.getVersionModels); export const getComparisonModelsLoaded = createSelector(getCompareState, fromCompare.getVersionModelsLoaded); export const getComparisonFilterId = createSelector(getCompareState, fromCompare.getFilterId); + +export const getComparisonModelsFilteredFn = (models) => models.map((model) => ({ + ...model, + metadataFilters: getVersionModelFiltersFn(model, model.type) +})); + +export const getComparisonModelsFiltered = createSelector(getComparisonModels, getComparisonModelsFilteredFn); + export const getComparisonConfigurations = createSelector( - getComparisonModels, + getComparisonModelsFiltered, getConfigurationDefinition, getConfigurationSchema, getConfigurationSectionsFn @@ -290,9 +307,7 @@ export const getComparisonSelectedFilters = createSelector( // Version Restoration export const getRestoreState = createSelector(getState, getRestoreStateFn); - export const getVersionState = createSelector(getState, getVersionStateFn); - export const getVersionLoading = createSelector(getVersionState, fromVersion.isVersionLoading); export const getVersionModel = createSelector(getVersionState, fromVersion.getVersionModel); diff --git a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts index 9ef3bf2bb..2096ac38b 100644 --- a/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/dynamic-http.provider.form.ts @@ -1,21 +1,13 @@ import { Wizard } from '../../../wizard/model'; import { DynamicHttpMetadataProvider } from '../../domain/model/providers/dynamic-http-metadata-provider'; import { BaseMetadataProviderEditor } from './base.provider.form'; +import { metadataFilterProcessor } from './utilities'; export const DynamicHttpMetadataProviderWizard: Wizard = { ...BaseMetadataProviderEditor, label: 'DynamicHttpMetadataProvider', type: 'DynamicHttpMetadataResolver', - formatter: (changes: DynamicHttpMetadataProvider) => { - let base = BaseMetadataProviderEditor.formatter(changes); - if (base.dynamicMetadataResolverAttributes) { - if (base.dynamicMetadataResolverAttributes.refreshDelayFactor) { - base.dynamicMetadataResolverAttributes.refreshDelayFactor = - base.dynamicMetadataResolverAttributes.refreshDelayFactor.toString(); - } - } - return base; - }, + schemaPreprocessor: metadataFilterProcessor, getValidators(namesList: string[] = [], xmlIdList: string[] = []): any { const validators = BaseMetadataProviderEditor.getValidators(namesList, xmlIdList); @@ -97,9 +89,7 @@ export const DynamicHttpMetadataProviderWizard: Wizard = { ...BaseMetadataProviderEditor, label: 'FileBackedHttpMetadataProvider', type: 'FileBackedHttpMetadataResolver', - formatter: (changes: FileBackedHttpMetadataProvider) => { - let base = BaseMetadataProviderEditor.formatter(changes); - if (base.reloadableMetadataResolverAttributes) { - if (base.reloadableMetadataResolverAttributes.refreshDelayFactor) { - base.reloadableMetadataResolverAttributes.refreshDelayFactor = - base.reloadableMetadataResolverAttributes.refreshDelayFactor.toString(); - } - } - return base; - }, + schemaPreprocessor: metadataFilterProcessor, getValidators(namesList: string[] = [], xmlIdList: string[] = []): any { const validators = BaseMetadataProviderEditor.getValidators(namesList, xmlIdList); validators['/metadataURL'] = (value, property, form) => { diff --git a/ui/src/app/metadata/provider/model/file-system.provider.form.ts b/ui/src/app/metadata/provider/model/file-system.provider.form.ts index fcf780ac5..9fcf7fb6d 100644 --- a/ui/src/app/metadata/provider/model/file-system.provider.form.ts +++ b/ui/src/app/metadata/provider/model/file-system.provider.form.ts @@ -6,16 +6,6 @@ export const FileSystemMetadataProviderWizard: Wizard { - let base = BaseMetadataProviderEditor.formatter(changes); - if (base.reloadableMetadataResolverAttributes) { - if (base.reloadableMetadataResolverAttributes.refreshDelayFactor) { - base.reloadableMetadataResolverAttributes.refreshDelayFactor = - base.reloadableMetadataResolverAttributes.refreshDelayFactor.toString(); - } - } - return base; - }, schema: '/api/ui/MetadataResolver/FilesystemMetadataResolver', steps: [ { diff --git a/ui/src/app/metadata/provider/model/local-dynamic.provider.form.ts b/ui/src/app/metadata/provider/model/local-dynamic.provider.form.ts index c53562873..c79bc487d 100644 --- a/ui/src/app/metadata/provider/model/local-dynamic.provider.form.ts +++ b/ui/src/app/metadata/provider/model/local-dynamic.provider.form.ts @@ -7,16 +7,6 @@ export const LocalDynamicMetadataProviderWizard: Wizard { - let base = BaseMetadataProviderEditor.formatter(changes); - if (base.dynamicMetadataResolverAttributes) { - if (base.dynamicMetadataResolverAttributes.refreshDelayFactor) { - base.dynamicMetadataResolverAttributes.refreshDelayFactor = - base.dynamicMetadataResolverAttributes.refreshDelayFactor.toString(); - } - } - return base; - }, schema: '/api/ui/MetadataResolver/LocalDynamicMetadataResolver', steps: [ { diff --git a/ui/src/app/metadata/provider/model/utilities.ts b/ui/src/app/metadata/provider/model/utilities.ts new file mode 100644 index 000000000..aea90bc21 --- /dev/null +++ b/ui/src/app/metadata/provider/model/utilities.ts @@ -0,0 +1,25 @@ +export const metadataFilterProcessor = (schema) => { + console.log(schema); + if (!schema) { + return null; + } + if (!schema.properties || !schema.properties.metadataFilters) { + return schema; + } + const filters = schema.properties.metadataFilters; + const processed = ({ + ...schema, + properties: { + ...schema.properties, + metadataFilters: { + type: 'object', + properties: filters.items.reduce((collection, filterType) => ({ + ...collection, + [filterType.$id]: filterType + }), {}) + } + } + }); + console.log(processed); + return processed; +}; diff --git a/ui/src/app/schema-form/registry.ts b/ui/src/app/schema-form/registry.ts index 266aeb9a2..1551a2497 100644 --- a/ui/src/app/schema-form/registry.ts +++ b/ui/src/app/schema-form/registry.ts @@ -20,6 +20,7 @@ import { CustomObjectWidget } from './widget/object/object.component'; import { CustomRadioComponent } from './widget/radio/radio.component'; import { InlineObjectListComponent } from './widget/array/inline-obj-list.component'; import { InlineObjectComponent } from './widget/object/inline-obj.component'; +import { CustomFloatComponent } from './widget/number/float.component'; export class CustomWidgetRegistry extends WidgetRegistry { constructor() { @@ -55,6 +56,7 @@ export class CustomWidgetRegistry extends WidgetRegistry { this.register('integer', CustomIntegerComponent); this.register('number', CustomIntegerComponent); + this.register('float', CustomFloatComponent); this.register('datalist', DatalistComponent); diff --git a/ui/src/app/schema-form/schema-form.module.ts b/ui/src/app/schema-form/schema-form.module.ts index 842e903dd..6b3d45751 100644 --- a/ui/src/app/schema-form/schema-form.module.ts +++ b/ui/src/app/schema-form/schema-form.module.ts @@ -15,6 +15,7 @@ import { CustomCheckboxComponent } from './widget/check/checkbox.component'; import { CustomTextAreaComponent } from './widget/textarea/textarea.component'; import { CustomArrayComponent } from './widget/array/array.component'; import { CustomIntegerComponent } from './widget/number/number.component'; +import { CustomFloatComponent } from './widget/number/float.component'; import { FilterTargetComponent } from './widget/filter-target/filter-target.component'; import { ChecklistComponent } from './widget/check/checklist.component'; import { IconButtonComponent } from './widget/button/icon-button.component'; @@ -34,6 +35,7 @@ export const COMPONENTS = [ CustomTextAreaComponent, CustomArrayComponent, CustomIntegerComponent, + CustomFloatComponent, FilterTargetComponent, ChecklistComponent, IconButtonComponent, diff --git a/ui/src/app/schema-form/widget/number/float.component.html b/ui/src/app/schema-form/widget/number/float.component.html new file mode 100644 index 000000000..56e15bc69 --- /dev/null +++ b/ui/src/app/schema-form/widget/number/float.component.html @@ -0,0 +1,25 @@ +
+ + {{schema.description}} + +
diff --git a/ui/src/app/schema-form/widget/number/float.component.ts b/ui/src/app/schema-form/widget/number/float.component.ts new file mode 100644 index 000000000..4a465a6ef --- /dev/null +++ b/ui/src/app/schema-form/widget/number/float.component.ts @@ -0,0 +1,68 @@ +import { + Component, AfterViewInit, ViewChild, ElementRef, +} from '@angular/core'; +import { IntegerWidget } from 'ngx-schema-form'; +import { SchemaService } from '../../service/schema.service'; + +@Component({ + selector: 'float-component', + templateUrl: `./float.component.html` +}) +export class CustomFloatComponent extends IntegerWidget implements AfterViewInit { + private _displayValue: string; + @ViewChild('input') element: ElementRef; + + constructor( + private widgetService: SchemaService + ) { + super(); + } + + ngAfterViewInit() { + super.ngAfterViewInit(); + const control = this.control; + this.formProperty.valueChanges.subscribe((newValue) => { + if (typeof this._displayValue !== 'undefined') { + // Ignore the model value, use the display value instead + if (control.value !== this._displayValue) { + control.setValue(this._displayValue, { emitEvent: false }); + } + } else { + if (control.value !== newValue) { + control.setValue(newValue, { emitEvent: false }); + } + } + }); + this.formProperty.errorsChanges.subscribe((errors) => { + control.setErrors(errors, { emitEvent: true }); + const messages = (errors || []) + .filter(e => { + return e.path && e.path.slice(1) === this.formProperty.path; + }) + .map(e => e.message); + this.errorMessages = messages.filter((m, i) => messages.indexOf(m) === i); + }); + control.valueChanges.subscribe((newValue) => { + const native = (this.element.nativeElement); + this._displayValue = newValue; + this.formProperty.setValue(newValue, false); + if (newValue === '' && native.validity.badInput) { + this.formProperty.extendErrors([{ + code: 'INVALID_NUMBER', + path: `#${this.formProperty.path}`, + message: 'Invalid number', + }]); + } + }); + } + + get required(): boolean { + return this.widgetService.isRequired(this.formProperty); + } + + get minimum(): number { + return this.required ? + this.schema.minimum : + this.formProperty.value === null ? null : this.schema.minimum; + } +} diff --git a/ui/src/app/wizard/model/form-definition.ts b/ui/src/app/wizard/model/form-definition.ts index bde8e10ad..5e3cbb6ea 100644 --- a/ui/src/app/wizard/model/form-definition.ts +++ b/ui/src/app/wizard/model/form-definition.ts @@ -10,4 +10,5 @@ export interface FormDefinition { parser(changes: Partial, schema?: any); formatter(changes: Partial, schema?: any); getValidators?(...args: any[]): { [key: string]: any }; + schemaPreprocessor?(schema: any): any; } diff --git a/ui/src/app/wizard/reducer/index.ts b/ui/src/app/wizard/reducer/index.ts index d85d930be..027ac1673 100644 --- a/ui/src/app/wizard/reducer/index.ts +++ b/ui/src/app/wizard/reducer/index.ts @@ -128,8 +128,12 @@ export const getLockedStatus = createSelector(getState, fromWizard.getLocked); export const getSchemaLockedFn = (step, locked) => step ? step.locked ? locked : false : false; export const getLocked = createSelector(getCurrent, getLockedStatus, getSchemaLockedFn); +export const getSchemaProcessedFn = (schema, definition) => + definition.schemaPreprocessor ? definition.schemaPreprocessor(schema) : schema; + export const getSchemaObject = createSelector(getState, fromWizard.getSchema); -export const getParsedSchema = createSelector(getSchemaObject, getLocked, getSchemaParseFn); +export const getProcessedSchema = createSelector(getSchemaObject, getWizardDefinition, getSchemaProcessedFn); +export const getParsedSchema = createSelector(getProcessedSchema, getLocked, getSchemaParseFn); export const getSchema = createSelector(getParsedSchema, getCurrent, getSplitSchema); diff --git a/ui/src/assets/schema/provider/filebacked-http.schema.json b/ui/src/assets/schema/provider/filebacked-http.schema.json index 0422ffbd2..505fe6ebd 100644 --- a/ui/src/assets/schema/provider/filebacked-http.schema.json +++ b/ui/src/assets/schema/provider/filebacked-http.schema.json @@ -3,6 +3,7 @@ "order": [ "name", "@type", + "enabled", "xmlId", "metadataURL", "initializeFromBackupFile", @@ -11,7 +12,10 @@ "requireValidMetadata", "failFastInitialization", "useDefaultPredicateRegistry", - "satisfyAnyPredicates" + "satisfyAnyPredicates", + "httpMetadataResolverAttributes", + "reloadableMetadataResolverAttributes", + "metadataFilters" ], "required": [ "name", @@ -190,6 +194,7 @@ "default": true }, "satisfyAnyPredicates": { + "$id": "satisfyAnyPredicates", "title": "label.satisfy-any-predicates", "description": "tooltip.satisfy-any-predicates", "type": "boolean", @@ -213,6 +218,7 @@ "default": false }, "httpMetadataResolverAttributes": { + "$id": "httpMetadataResolverAttributes", "order": [], "type": "object", "fieldsets": [ @@ -449,6 +455,7 @@ } }, "reloadableMetadataResolverAttributes": { + "$id": "reloadableMetadataResolverAttributes", "type": "object", "properties": { "minRefreshDelay": { @@ -498,25 +505,27 @@ "refreshDelayFactor": { "title": "label.refresh-delay-factor", "description": "tooltip.refresh-delay-factor", - "type": "string", + "type": "number", "widget": { - "id": "string", - "help": "message.real-number" + "id": "float", + "help": "message.real-number", + "step": 0.01 }, "placeholder": "label.real-number", - "minimum": 0, - "maximum": 1, - "default": "", - "pattern": "^(?:([0]*(\\.[0-9]+)?|[0]*\\.[0-9]*[1-9][0-9]*)|)$" + "minimum": 0.001, + "maximum": 0.999 } } }, "metadataFilters": { + "$id": "metadataFilters", "title": "", "description": "", - "type": "object", - "properties": { - "RequiredValidUntil": { + "type": "array", + "additionalItems": true, + "items": [ + { + "$id": "RequiredValidUntil", "title": "label.required-valid-until", "type": "object", "widget": { @@ -532,10 +541,14 @@ "id": "datalist", "data": [ "PT0S", - "P14D", - "P7D", - "P1D", - "PT12H" + "PT30S", + "PT1M", + "PT10M", + "PT30M", + "PT1H", + "PT4H", + "PT12H", + "PT24H" ] }, "default": null, @@ -543,7 +556,8 @@ } } }, - "SignatureValidation": { + { + "$id": "SignatureValidation", "title": "label.signature-validation-filter", "type": "object", "widget": { @@ -559,8 +573,7 @@ "certificateFile": { "title": "label.certificate-file", "description": "tooltip.certificate-file", - "type": "string", - "widget": "textline" + "type": "string" } }, "anyOf": [ @@ -570,10 +583,6 @@ "enum": [ true ] - }, - "certificateFile": { - "minLength": 1, - "type": "string" } }, "required": [ @@ -591,7 +600,8 @@ } ] }, - "EntityRoleWhiteList": { + { + "$id": "EntityRoleWhiteList", "title": "label.entity-role-whitelist", "type": "object", "widget": { @@ -610,13 +620,13 @@ "oneOf": [ { "enum": [ - "md:SPSSODescriptor" + "SPSSODescriptor" ], "description": "value.spdescriptor" }, { "enum": [ - "md:AttributeAuthorityDescriptor" + "AttributeAuthorityDescriptor" ], "description": "value.attr-auth-descriptor" } @@ -637,7 +647,7 @@ } } } - } + ] } } } \ No newline at end of file