Basic validation of nodes which are not Id
This commit is contained in:
parent
2f11941ebd
commit
1cc64d2ae9
@ -5,11 +5,13 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.oneedtech.inspect.schema.Catalog;
|
||||
import org.oneedtech.inspect.schema.SchemaKey;
|
||||
import org.oneedtech.inspect.util.resource.Resource;
|
||||
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
|
||||
import org.oneedtech.inspect.vc.util.PrimitiveValueValidator;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.google.common.base.MoreObjects;
|
||||
@ -125,23 +127,33 @@ public class Assertion extends Credential {
|
||||
}
|
||||
|
||||
public enum ValueType {
|
||||
BOOLEAN,
|
||||
COMPACT_IRI,
|
||||
DATA_URI,
|
||||
DATA_URI_OR_URL,
|
||||
DATETIME,
|
||||
EMAIL,
|
||||
ID,
|
||||
IDENTITY_HASH,
|
||||
IRI,
|
||||
LANGUAGE,
|
||||
MARKDOWN_TEXT,
|
||||
RDF_TYPE,
|
||||
TELEPHONE,
|
||||
TEXT,
|
||||
TEXT_OR_NUMBER,
|
||||
URL,
|
||||
URL_AUTHORITY;
|
||||
BOOLEAN(PrimitiveValueValidator::validateBoolean),
|
||||
COMPACT_IRI(PrimitiveValueValidator::validateCompactIri),
|
||||
DATA_URI(PrimitiveValueValidator::validateDataUri),
|
||||
DATA_URI_OR_URL(PrimitiveValueValidator::validateDataUriOrUrl),
|
||||
DATETIME(PrimitiveValueValidator::validateDatetime),
|
||||
EMAIL(PrimitiveValueValidator::validateEmail),
|
||||
ID(null),
|
||||
IDENTITY_HASH(PrimitiveValueValidator::validateIdentityHash),
|
||||
IRI(PrimitiveValueValidator::validateIri),
|
||||
LANGUAGE(PrimitiveValueValidator::validateLanguage),
|
||||
MARKDOWN_TEXT(PrimitiveValueValidator::validateMarkdown),
|
||||
RDF_TYPE(PrimitiveValueValidator::validateRdfType),
|
||||
TELEPHONE(PrimitiveValueValidator::validateTelephone),
|
||||
TEXT(PrimitiveValueValidator::validateText),
|
||||
TEXT_OR_NUMBER(PrimitiveValueValidator::validateTextOrNumber),
|
||||
URL(PrimitiveValueValidator::validateUrl),
|
||||
URL_AUTHORITY(PrimitiveValueValidator::validateUrlAuthority);
|
||||
|
||||
private final Function<JsonNode, Boolean> validationFunction;
|
||||
|
||||
private ValueType(Function<JsonNode, Boolean> validationFunction) {
|
||||
this.validationFunction = validationFunction;
|
||||
}
|
||||
|
||||
public Function<JsonNode, Boolean> getValidationFunction() {
|
||||
return validationFunction;
|
||||
}
|
||||
|
||||
public static List<ValueType> primitives = List.of(BOOLEAN, DATA_URI_OR_URL, DATETIME, ID, IDENTITY_HASH, IRI, LANGUAGE, MARKDOWN_TEXT,
|
||||
TELEPHONE, TEXT, TEXT_OR_NUMBER, URL, URL_AUTHORITY);
|
||||
|
@ -21,7 +21,6 @@ import org.oneedtech.inspect.util.json.ObjectMapperCache;
|
||||
import org.oneedtech.inspect.util.resource.Resource;
|
||||
import org.oneedtech.inspect.util.resource.ResourceType;
|
||||
import org.oneedtech.inspect.util.spec.Specification;
|
||||
import org.oneedtech.inspect.vc.Assertion.Type;
|
||||
import org.oneedtech.inspect.vc.Credential.CredentialEnum;
|
||||
import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
|
||||
import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
|
||||
@ -31,6 +30,7 @@ import org.oneedtech.inspect.vc.payload.SvgParser;
|
||||
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.util.CachingDocumentLoader;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
@ -116,6 +116,21 @@ 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
|
||||
List<Validation> validations = assertion.getValidations();
|
||||
for (Validation validation : validations) {
|
||||
probeCount++;
|
||||
accumulator.add(new ValidationPropertyProbe(validation).run(assertion.getJson(), ctx));
|
||||
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
|
||||
}
|
||||
|
||||
// Each Badge Object contains all required properties for its class
|
||||
// This could be done validating with the schema, but seems that there are some error on that file
|
||||
// So, we do a manual Probe for the nodes
|
||||
|
||||
// accumulator.add(new RequiredFieldsProbe(jsonLdGeneratedObject).run(assertion, ctx));
|
||||
// if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
|
||||
|
||||
//canonical schema and inline schemata
|
||||
// SchemaKey schema = assertion.getSchemaKey().orElseThrow();
|
||||
// for(Probe<JsonNode> probe : List.of(new JsonSchemaProbe(schema), new InlineJsonSchemaProbe(schema))) {
|
||||
|
@ -36,6 +36,55 @@ public class Validation {
|
||||
this.fullValidate = builder.fullValidate;
|
||||
}
|
||||
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Assertion.ValueType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean isRequired() {
|
||||
return required;
|
||||
}
|
||||
|
||||
public boolean isMany() {
|
||||
return many;
|
||||
}
|
||||
|
||||
public List<String> getMustContainOne() {
|
||||
return mustContainOne;
|
||||
}
|
||||
|
||||
public List<String> getPrerequisites() {
|
||||
return prerequisites;
|
||||
}
|
||||
|
||||
public List<Assertion.Type> getExpectedTypes() {
|
||||
return expectedTypes;
|
||||
}
|
||||
|
||||
public boolean isAllowRemoteUrl() {
|
||||
return allowRemoteUrl;
|
||||
}
|
||||
|
||||
public boolean isAllowDataUri() {
|
||||
return allowDataUri;
|
||||
}
|
||||
|
||||
public boolean isFetch() {
|
||||
return fetch;
|
||||
}
|
||||
|
||||
public String getDefaultType() {
|
||||
return defaultType;
|
||||
}
|
||||
|
||||
public boolean isFullValidate() {
|
||||
return fullValidate;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String name;
|
||||
private Assertion.ValueType type;
|
||||
|
@ -0,0 +1,135 @@
|
||||
package org.oneedtech.inspect.vc.probe;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
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.Assertion.ValueType;
|
||||
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
|
||||
public class ValidationPropertyProbe extends PropertyProbe {
|
||||
private final Validation validation;
|
||||
|
||||
public ValidationPropertyProbe(Validation validation) {
|
||||
super(ID + "<" + validation.getName() + ">", validation.getName());
|
||||
this.validation = validation;
|
||||
setValidations(this::validate);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
|
||||
if (validation.isRequired()) {
|
||||
return error("Required property " + validation.getName() + " not present in " + node.toPrettyString(), ctx);
|
||||
} else {
|
||||
// optional property
|
||||
return success(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates presence and data type of a single property that is
|
||||
* expected to be one of the Open Badges Primitive data types or an ID.
|
||||
* @param node node to check data type
|
||||
* @param ctx associated run context
|
||||
* @return validation result
|
||||
*/
|
||||
private ReportItems validate(JsonNode node, RunContext ctx) {
|
||||
// required property
|
||||
if (validation.isRequired()) {
|
||||
if (node.isObject()) {
|
||||
if (!node.fieldNames().hasNext()) {
|
||||
return error("Required property " + validation.getName() + " value " + node.toString() + " is not acceptable", ctx);
|
||||
}
|
||||
} else {
|
||||
List<String> values = JsonNodeUtil.asStringList(node);
|
||||
if (values == null ||values.isEmpty()) {
|
||||
return error("Required property " + validation.getName() + " value " + values + " is not acceptable", ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<JsonNode> nodeList = JsonNodeUtil.asNodeList(node);
|
||||
// many property
|
||||
if (!validation.isMany()) {
|
||||
if (nodeList.size() > 1) {
|
||||
return error("Property " + validation.getName() + "has more than the single allowed value.", ctx);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (validation.getType() != ValueType.ID) {
|
||||
Function<JsonNode, Boolean> validationFunction = validation.getType().getValidationFunction();
|
||||
for (JsonNode childNode : nodeList) {
|
||||
Boolean valid = validationFunction.apply(childNode);
|
||||
if (!valid) {
|
||||
return error(validation.getType() + " property " + validation.getName() + " value " + childNode.toString() + " not valid", ctx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/**
|
||||
for i in range(len(values_to_test)):
|
||||
val = values_to_test[i]
|
||||
if isinstance(prop_value, (list, tuple,)):
|
||||
value_to_test_path = [node_id, prop_name, i]
|
||||
else:
|
||||
value_to_test_path = [node_id, prop_name]
|
||||
|
||||
if isinstance(val, dict):
|
||||
actions.append(
|
||||
add_task(VALIDATE_EXPECTED_NODE_CLASS, node_path=value_to_test_path,
|
||||
expected_class=task_meta.get('expected_class'),
|
||||
full_validate=task_meta.get('full_validate', True)))
|
||||
continue
|
||||
elif task_meta.get('allow_data_uri') and not PrimitiveValueValidator(ValueTypes.DATA_URI_OR_URL)(val):
|
||||
raise ValidationError("ID-type property {} had value `{}` that isn't URI or DATA URI in {}.".format(
|
||||
prop_name, abv(val), abv_node(node_id, node_path))
|
||||
)
|
||||
elif not task_meta.get('allow_data_uri', False) and not PrimitiveValueValidator(ValueTypes.IRI)(val):
|
||||
actions.append(report_message(
|
||||
"ID-type property {} had value `{}` where another scheme may have been expected {}.".format(
|
||||
prop_name, abv(val), abv_node(node_id, node_path)
|
||||
), message_level=MESSAGE_LEVEL_WARNING))
|
||||
raise ValidationError(
|
||||
"ID-type property {} had value `{}` not embedded node or in IRI format in {}.".format(
|
||||
prop_name, abv(val), abv_node(node_id, node_path))
|
||||
)
|
||||
try:
|
||||
target = get_node_by_id(state, val)
|
||||
except IndexError:
|
||||
if not task_meta.get('fetch', False):
|
||||
if task_meta.get('allow_remote_url') and PrimitiveValueValidator(ValueTypes.URL)(val):
|
||||
continue
|
||||
if task_meta.get('allow_data_uri') and PrimitiveValueValidator(ValueTypes.DATA_URI)(val):
|
||||
continue
|
||||
raise ValidationError(
|
||||
'Node {} has {} property value `{}` that appears not to be in URI format'.format(
|
||||
abv_node(node_id, node_path), prop_name, abv(val)
|
||||
) + ' or did not correspond to a known local node.')
|
||||
else:
|
||||
actions.append(
|
||||
add_task(FETCH_HTTP_NODE, url=val,
|
||||
expected_class=task_meta.get('expected_class'),
|
||||
source_node_path=value_to_test_path
|
||||
))
|
||||
else:
|
||||
actions.append(
|
||||
add_task(VALIDATE_EXPECTED_NODE_CLASS, node_id=val,
|
||||
expected_class=task_meta.get('expected_class')))
|
||||
*/
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
return fatal(t.getMessage(), ctx);
|
||||
}
|
||||
|
||||
return success(ctx);
|
||||
}
|
||||
|
||||
public static final String ID = ValidationPropertyProbe.class.getSimpleName();
|
||||
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
package org.oneedtech.inspect.vc.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.IllformedLocaleException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
|
||||
|
||||
import com.apicatalog.jsonld.JsonLd;
|
||||
import com.apicatalog.jsonld.JsonLdError;
|
||||
import com.apicatalog.jsonld.document.JsonDocument;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectReader;
|
||||
import com.google.common.io.Resources;
|
||||
|
||||
import jakarta.json.JsonArray;
|
||||
import jakarta.json.JsonObject;
|
||||
|
||||
/**
|
||||
* Validator for ValueType. Translated into java from PrimitiveValueValidator in validation.py
|
||||
*/
|
||||
public class PrimitiveValueValidator {
|
||||
|
||||
public static boolean validateBoolean(JsonNode value) {
|
||||
return value.isValueNode() && value.isBoolean();
|
||||
}
|
||||
|
||||
public static boolean validateCompactIri(JsonNode value) {
|
||||
if (value.asText().equals("id") || validateIri(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean validateDataUri(JsonNode value) {
|
||||
try {
|
||||
URI uri = new URI(value.asText());
|
||||
return "data".equalsIgnoreCase(uri.getScheme());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean validateDataUriOrUrl(JsonNode value) {
|
||||
return validateUrl(value) || validateDataUri(value);
|
||||
}
|
||||
|
||||
public static boolean validateDatetime(JsonNode value) {
|
||||
try {
|
||||
DateTimeFormatter.ISO_INSTANT.parse(value.asText());
|
||||
return true;
|
||||
} catch (DateTimeParseException | NullPointerException ignored) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean validateEmail(JsonNode value) {
|
||||
return value.asText().matches("(^[^@\\s]+@[^@\\s]+$)");
|
||||
}
|
||||
|
||||
public static boolean is_hashed_identity_hash(JsonNode value) {
|
||||
return value.asText().matches("md5\\$[\\da-fA-F]{32}$") || value.asText().matches("sha256\\$[\\da-fA-F]{64}$");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that identity is a string. More specific rules may only be enforced at the class instance level.
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
public static boolean validateIdentityHash(JsonNode value) {
|
||||
return validateText(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string matches an acceptable IRI format and scheme. For now, only accepts a few schemes,
|
||||
* 'http', 'https', blank node identifiers, and 'urn:uuid'
|
||||
|
||||
* @return
|
||||
*/
|
||||
public static boolean validateIri(JsonNode value) {
|
||||
return validateUrl(value) || value.asText().matches("^_:") || value.asText().matches("^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$");
|
||||
}
|
||||
|
||||
public static boolean validateLanguage(JsonNode value) {
|
||||
try {
|
||||
return validateText(value) && new Locale.Builder().setLanguageTag(value.asText()).build() != null;
|
||||
} catch (IllformedLocaleException ignored) {
|
||||
// value is not a valid locale
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean validateMarkdown(JsonNode value) {
|
||||
return validateText(value);
|
||||
}
|
||||
|
||||
public static boolean validateRdfType(JsonNode value) {
|
||||
if (!validateText(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper(); // TODO: get from RunContext
|
||||
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
|
||||
|
||||
try {
|
||||
JsonNode node = mapper.readTree(Resources.getResource("contexts/ob-v2p0.json"));
|
||||
ObjectReader readerForUpdating = mapper.readerForUpdating(node);
|
||||
JsonNode merged = readerForUpdating.readValue("{\"type\": \"" + value.asText() + "\"}");
|
||||
|
||||
JsonDocument jsonDocument = JsonDocument.of(new StringReader(merged.toString()));
|
||||
JsonNode expanded = mapper.readTree(JsonLd.expand(jsonDocument).get().toString());
|
||||
|
||||
return validateIri(JsonNodeUtil.asNodeList(expanded, "$[0].@type[0]", jsonPath).get(0));
|
||||
|
||||
} catch (NullPointerException | IOException | JsonLdError e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean validateTelephone(JsonNode value) {
|
||||
return value.asText().matches("^\\+?[1-9]\\d{1,14}(;ext=\\d+)?$");
|
||||
}
|
||||
|
||||
public static boolean validateText(JsonNode value) {
|
||||
return value.isValueNode() && value.isTextual();
|
||||
}
|
||||
|
||||
public static boolean validateTextOrNumber(JsonNode value) {
|
||||
return value.isValueNode() && value.isTextual() || value.isNumber();
|
||||
}
|
||||
|
||||
public static boolean validateUrl(JsonNode value) {
|
||||
if (!value.isValueNode()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
new URL(value.asText());
|
||||
return true;
|
||||
} catch (MalformedURLException ignored) {
|
||||
// value is not a valid URL
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean validateUrlAuthority(JsonNode value) {
|
||||
if (!validateText(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
URI testUri;
|
||||
try {
|
||||
testUri = new URI("http://" + value.asText() + "/test");
|
||||
String host = testUri.getHost();
|
||||
if (host == null || !host.matches("(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{0,62}[a-zA-Z0-9]\\.)+[a-zA-Z]{2,63}$)")) {
|
||||
return false;
|
||||
}
|
||||
return testUri.getScheme().equals("http") && host.equals(value.asText()) && testUri.getPath().equals("/test") && testUri.getQuery() == null;
|
||||
} catch (URISyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user