From d3667e580c0a9376decbcb4f8c0cf38ded30c5fc Mon Sep 17 00:00:00 2001 From: Xavi Aracil Date: Wed, 30 Nov 2022 08:24:40 +0100 Subject: [PATCH] New validation probe for rdf types --- .../oneedtech/inspect/vc/OB20Inspector.java | 6 +- .../vc/probe/ValidationPropertyProbe.java | 153 +++++++++++++++++- .../probe/ValidationPropertyProbeFactory.java | 24 +++ .../probe/ValidationRdfTypePropertyProbe.java | 58 +++++++ 4 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbeFactory.java create mode 100644 inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationRdfTypePropertyProbe.java diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java index 7d06bc7..f2c70e5 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java @@ -31,6 +31,7 @@ import org.oneedtech.inspect.vc.probe.ContextPropertyProbe; import org.oneedtech.inspect.vc.probe.CredentialParseProbe; import org.oneedtech.inspect.vc.probe.TypePropertyProbe; import org.oneedtech.inspect.vc.probe.ValidationPropertyProbe; +import org.oneedtech.inspect.vc.probe.ValidationPropertyProbeFactory; import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import com.apicatalog.jsonld.loader.DocumentLoader; @@ -119,11 +120,12 @@ public class OB20Inspector extends Inspector { accumulator.add(new JsonLDValidationProbe(jsonLdGeneratedObject).run(assertion, ctx)); if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount); - // Validates the Open Badge + // Validates the Open Badge, from the compacted form + JsonNode assertionNode = mapper.readTree(jsonLdGeneratedObject.getJson()); List validations = assertion.getValidations(); for (Validation validation : validations) { probeCount++; - accumulator.add(new ValidationPropertyProbe(validation).run(assertion.getJson(), ctx)); + accumulator.add(ValidationPropertyProbeFactory.of(validation).run(assertionNode, ctx)); if(broken(accumulator)) return abort(ctx, accumulator, probeCount); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbe.java index f23760c..b9edbd1 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbe.java @@ -32,8 +32,8 @@ import foundation.identity.jsonld.ConfigurableDocumentLoader; public class ValidationPropertyProbe extends PropertyProbe { - private final Validation validation; - private final boolean fullValidate; // TODO: fullValidate + protected final Validation validation; + protected final boolean fullValidate; // TODO: fullValidate public ValidationPropertyProbe(Validation validation) { this(validation, false); @@ -46,8 +46,6 @@ public class ValidationPropertyProbe extends PropertyProbe { setValidations(this::validate); } - - @Override protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) { if (validation.isRequired()) { @@ -65,7 +63,7 @@ public class ValidationPropertyProbe extends PropertyProbe { * @param ctx associated run context * @return validation result */ - private ReportItems validate(JsonNode node, RunContext ctx) { + protected ReportItems validate(JsonNode node, RunContext ctx) { ReportItems result = new ReportItems(); // required property @@ -172,7 +170,7 @@ public class ValidationPropertyProbe extends PropertyProbe { private ReportItems validateExpectedTypes(JsonNode node, RunContext ctx) { List results = validation.getExpectedTypes().stream() .flatMap(type -> type.getValidations().stream()) - .map(v -> new ValidationPropertyProbe(v, validation.isFullValidate())) + .map(v -> ValidationPropertyProbeFactory.of(v, validation.isFullValidate())) .map(probe -> { try { return probe.run(node, ctx); @@ -183,5 +181,148 @@ public class ValidationPropertyProbe extends PropertyProbe { .collect(Collectors.toList()); return new ReportItems(results); } + + private void flattenEmbeddedResource() { + /* + try: + node_id = task_meta['node_id'] + node = get_node_by_id(state, node_id) + prop_name = task_meta['prop_name'] + node_class = task_meta['node_class'] + except (IndexError, KeyError): + raise TaskPrerequisitesError() + + actions = [] + value = node.get(prop_name) + if value is None: + return task_result(True, "Expected property {} was missing in node {}".format(node_id)) + + if isinstance(value, six.string_types): + return task_result( + True, "Property {} referenced from {} is not embedded in need of flattening".format( + prop_name, abv_node(node_id=node_id) + )) + + if not isinstance(value, dict): + return task_result( + False, "Property {} referenced from {} is not a JSON object or string as expected".format( + prop_name, abv_node(node_id=node_id) + )) + embedded_node_id = value.get('id') + + if embedded_node_id is None: + new_node = value.copy() + embedded_node_id = '_:{}'.format(uuid.uuid4()) + new_node['id'] = embedded_node_id + new_node['@context'] = OPENBADGES_CONTEXT_V2_URI + actions.append(add_node(embedded_node_id, data=new_node)) + actions.append(patch_node(node_id, {prop_name: embedded_node_id})) + actions.append(report_message( + 'Node id missing at {}. A blank node ID has been assigned'.format( + abv_node(node_path=[node_id, prop_name], length=64) + ), message_level=MESSAGE_LEVEL_WARNING) + ) + elif not isinstance(embedded_node_id, six.string_types) or not is_iri(embedded_node_id): + return task_result(False, "Embedded JSON object at {} has no proper assigned id.".format( + abv_node(node_path=[node_id, prop_name]))) + + elif node_class == OBClasses.Assertion and not is_url(embedded_node_id): + if not re.match(URN_REGEX, embedded_node_id, re.IGNORECASE): + actions.append(report_message( + 'ID format for {} at {} not in an expected HTTP or URN:UUID scheme'.format( + embedded_node_id, abv_node(node_path=[node_id, prop_name]) + ))) + new_node = value.copy() + new_node['@context'] = OPENBADGES_CONTEXT_V2_URI + actions.append(add_node(embedded_node_id, data=value)) + actions.append(patch_node(node_id, {prop_name: embedded_node_id})) + + else: + actions.append(patch_node(node_id, {prop_name: embedded_node_id})) + if not node_match_exists(state, embedded_node_id) and not filter_tasks( + state, node_id=embedded_node_id, task_type=FETCH_HTTP_NODE): + # fetch + actions.append(add_task(FETCH_HTTP_NODE, url=embedded_node_id)) + + return task_result(True, "Embedded {} node in {} queued for storage and/or refetching as needed", actions) + + */ + } + + private void validateImage() { + /* +def validate_image(state, task_meta, **options): + try: + node_id = task_meta.get('node_id') + node_path = task_meta.get('node_path') + prop_name = task_meta.get('prop_name', 'image') + node_class = task_meta.get('node_class') + required = bool(task_meta.get('required', False)) + if node_id: + node = get_node_by_id(state, node_id) + node_path = [node_id] + else: + node = get_node_by_path(state, node_path) + + if options.get('cache_backend'): + session = requests_cache.CachedSession( + backend=options['cache_backend'], expire_after=options.get('cache_expire_after', 300)) + else: + session = requests.Session() + except (IndexError, TypeError, KeyError): + raise TaskPrerequisitesError() + + actions = [] + + image_val = node.get(prop_name) + + if image_val is None: + return task_result(not required, "Could not load and validate image in node {}".format(abv_node(node_id, node_path))) + if isinstance(image_val, six.string_types): + url = image_val + elif isinstance(image_val, dict): + url = image_val.get('id') + elif isinstance(image_val, list): + return task_result(False, "many images not allowed") + else: + raise TypeError("Could not interpret image property value {}".format( + abbreviate_value(image_val) + )) + if is_data_uri(url): + if task_meta.get('allow_data_uri', False) is False: + return task_result(False, "Image in node {} may not be a data URI.".format(abv_node(node_id, node_path))) + try: + mimetypes = re.match(r'(?P^data):(?P[^,]{0,}?)?(?Pbase64)?,(?P.*$)', url).group( + 'mimetypes') + if 'image/png' not in mimetypes and 'image/svg+xml' not in mimetypes: + raise ValueError("Disallowed filetype") + except (AttributeError, ValueError,): + return task_result( + False, "Data URI image does not declare any of the allowed PNG or SVG mime types in {}".format( + abv_node(node_id, node_path)) + ) + elif url: + existing_file = state.get('input', {}).get('original_json', {}).get(url) + if existing_file: + return task_result(True, "Image resource already stored for url {}".format(abbreviate_value(url))) + else: + try: + result = session.get( + url, headers={'Accept': 'application/ld+json, application/json, image/png, image/svg+xml'} + ) + result.raise_for_status() + content_type = result.headers['content-type'] + encoded_body = base64.b64encode(result.content) + data_uri = "data:{};base64,{}".format(content_type, encoded_body) + + except (requests.ConnectionError, requests.HTTPError, KeyError): + return task_result(False, "Could not fetch image at {}".format(url)) + else: + actions.append(store_original_resource(url, data_uri)) + + return task_result(True, "Validated image for node {}".format(abv_node(node_id, node_path)), actions) + + */ + } public static final String ID = ValidationPropertyProbe.class.getSimpleName(); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbeFactory.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbeFactory.java new file mode 100644 index 0000000..0156d32 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationPropertyProbeFactory.java @@ -0,0 +1,24 @@ +package org.oneedtech.inspect.vc.probe; + +import static org.oneedtech.inspect.util.code.Defensives.checkNotNull; + +import org.oneedtech.inspect.vc.Validation; +import org.oneedtech.inspect.vc.Assertion.ValueType; + +/** + * Factory for ValidationPropertyProbes + * @author xaracil + */ +public class ValidationPropertyProbeFactory { + public static ValidationPropertyProbe of(Validation validation) { + return of(validation, false); + } + + public static ValidationPropertyProbe of(Validation validation, boolean fullValidate) { + checkNotNull(validation.getType()); + if (validation.getType() == ValueType.RDF_TYPE) { + return new ValidationRdfTypePropertyProbe(validation, fullValidate); + } + return new ValidationPropertyProbe(validation, fullValidate); + } +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationRdfTypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationRdfTypePropertyProbe.java new file mode 100644 index 0000000..338820e --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/ValidationRdfTypePropertyProbe.java @@ -0,0 +1,58 @@ +package org.oneedtech.inspect.vc.probe; + +import java.util.List; +import java.util.stream.Collectors; + +import org.oneedtech.inspect.core.probe.Outcome; +import org.oneedtech.inspect.core.probe.RunContext; +import org.oneedtech.inspect.core.report.ReportItems; +import org.oneedtech.inspect.vc.Validation; +import org.oneedtech.inspect.vc.util.JsonNodeUtil; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.TextNode; + +public class ValidationRdfTypePropertyProbe extends ValidationPropertyProbe { + public ValidationRdfTypePropertyProbe(Validation validation) { + super(validation); + } + + public ValidationRdfTypePropertyProbe(Validation validation, boolean fullValidate) { + super(validation, fullValidate); + } + + @Override + protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) { + if (!validation.isRequired()) { + // check if we have a default type + if (validation.getDefaultType() != null) { + JsonNodeFactory factory = JsonNodeFactory.instance; + TextNode textNode = factory.textNode(validation.getDefaultType()); + // validate with default value + return validate(textNode, ctx); + } + } + + return error("Required property " + validation.getName() + " not present in " + node.toPrettyString(), ctx); + } + + @Override + protected ReportItems validate(JsonNode node, RunContext ctx) { + ReportItems result = super.validate(node, ctx); + if (result.contains(Outcome.ERROR, Outcome.FATAL)) { + return result; + } + if (!validation.getMustContainOne().isEmpty()) { + List values = JsonNodeUtil.asStringList(node); + boolean valid = validation.getMustContainOne().stream().anyMatch(type -> values.contains(type)); + if (!valid) { + return new ReportItems(List.of(result, + fatal("Node " + validation.getName() + " of type " + node.asText() + + " does not have type among allowed values (" + validation.getMustContainOne().stream().collect(Collectors.joining(",")) + ")", ctx) + )); + } + } + return new ReportItems(List.of(result, success(ctx))); + } +}