From bdf8e93330f4341f5420035dda6e3f0f95ae31ee Mon Sep 17 00:00:00 2001 From: Dmitriy Kopylenko Date: Thu, 22 Aug 2019 09:26:24 -0400 Subject: [PATCH] 1414: MetadataResolvers JSON schema validation WIP --- ...orSchemaValidatingControllerAdvice.groovy} | 20 +--- .../LowLevelJsonSchemaValidator.groovy | 58 ++++++++++ ...verSchemaValidatingControllerAdvice.groovy | 41 +++++++ ...lerSchemaValidationIntegrationTests.groovy | 100 ++++++++++++++++++ 4 files changed, 204 insertions(+), 15 deletions(-) rename backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/{RelyingPartyOverridesJsonSchemaValidatingControllerAdvice.groovy => EntityDescriptorSchemaValidatingControllerAdvice.groovy} (70%) create mode 100644 backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/LowLevelJsonSchemaValidator.groovy create mode 100644 backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataResolverSchemaValidatingControllerAdvice.groovy create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerSchemaValidationIntegrationTests.groovy 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..b80c1f5d6 --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/LowLevelJsonSchemaValidator.groovy @@ -0,0 +1,58 @@ +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.filesystemMetadataProviderSchema +import static edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaLocationLookup.localDynamicMetadataProviderSchema + +/** + * Currently uses mjson library. + */ +class LowLevelJsonSchemaValidator { + + static HttpInputMessage validatePayloadAgainstSchema(HttpInputMessage inputMessage, URI schemaUri) { + def json = extractJsonPayload(inputMessage) + def schema = Json.schema(schemaUri) + doValidate(schema, json) + } + + static HttpInputMessage validateMetadataResolverTypePayloadAgainstSchema(HttpInputMessage inputMessage, + JsonSchemaResourceLocationRegistry schemaRegistry) { + def json = extractJsonPayload(inputMessage) + 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 inputMessage + } + doValidate(Json.schema(schemaUri), json) + } + + private static Json extractJsonPayload(HttpInputMessage inputMessage) { + Json.read(new ByteArrayInputStream(inputMessage.body.bytes).getText()) + } + + private static HttpInputMessage doValidate(Json.Schema schema, Json json) { + def validationResult = schema.validate(json) + if (!validationResult.at('ok')) { + throw new JsonSchemaValidationFailedException(validationResult.at('errors').asList()) + } + return [ + getBody : { new ByteArrayInputStream(bytes) }, + getHeaders: { inputMessage.headers } + ] as HttpInputMessage + } +} diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataResolverSchemaValidatingControllerAdvice.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataResolverSchemaValidatingControllerAdvice.groovy new file mode 100644 index 000000000..6ecbef95a --- /dev/null +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/jsonschema/MetadataResolverSchemaValidatingControllerAdvice.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 MetadataResolverSchemaValidatingControllerAdvice 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/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..1f96de134 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolverControllerSchemaValidationIntegrationTests.groovy @@ -0,0 +1,100 @@ +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' + + 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 = 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) + } +}