Validate properties in Assertion

This commit is contained in:
Xavi Aracil 2022-11-29 18:06:36 +01:00
parent 1cc64d2ae9
commit 0d6d97cd4f
9 changed files with 146 additions and 84 deletions

View File

@ -1,6 +1,5 @@
package org.oneedtech.inspect.vc;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@ -27,7 +26,7 @@ public class Assertion extends Credential {
final Assertion.Type assertionType;
protected Assertion(Resource resource, JsonNode data, String jwt, Map<CredentialEnum, SchemaKey> schemas) {
super(ID, resource, data, jwt, schemas);
super(resource.getID(), resource, data, jwt, schemas);
JsonNode typeNode = jsonData.get("type");
this.assertionType = Assertion.Type.valueOf(typeNode);
@ -124,6 +123,10 @@ public class Assertion extends Credential {
public List<String> getContextUris() {
return List.of("https://w3id.org/openbadges/v2") ;
}
public List<Validation> getValidations() {
return validationMap.get(this);
}
}
public enum ValueType {
@ -165,7 +168,7 @@ public class Assertion extends Credential {
new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.Assertion)).build(),
new Validation.Builder().name("recipient").type(ValueType.ID).expectedType(Type.IdentityObject).required(true).build(),
new Validation.Builder().name("badge").type(ValueType.ID).prerequisite("ASN_FLATTEN_BC").expectedType(Type.BadgeClass).fetch(true).required(true).build(),
new Validation.Builder().name("verification").type(ValueType.ID).expectedTypes(List.of(Type.VerificationObjectAssertion)).required(true).build(),
new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectAssertion).required(true).build(),
new Validation.Builder().name("issuedOn").type(ValueType.DATETIME).required(true).build(),
new Validation.Builder().name("expires").type(ValueType.DATETIME).required(false).build(),
new Validation.Builder().name("image").type(ValueType.ID).required(false).allowRemoteUrl(true).expectedType(Type.Image).fetch(false).allowDataUri(false).build(),

View File

@ -33,6 +33,7 @@ import org.oneedtech.inspect.vc.probe.TypePropertyProbe;
import org.oneedtech.inspect.vc.probe.ValidationPropertyProbe;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import com.apicatalog.jsonld.loader.DocumentLoader;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -76,6 +77,7 @@ public class OB20Inspector extends Inspector {
ObjectMapper mapper = ObjectMapperCache.get(DEFAULT);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
DocumentLoader documentLoader = getDocumentLoader();
RunContext ctx = new RunContext.Builder()
.put(this)
@ -85,6 +87,7 @@ public class OB20Inspector extends Inspector {
.put(Key.GENERATED_OBJECT_BUILDER, new Assertion.Builder())
.put(Key.PNG_CREDENTIAL_KEY, PngParser.Keys.OB20)
.put(Key.SVG_CREDENTIAL_QNAME, SvgParser.QNames.OB20)
.put(Key.JSON_DOCUMENT_LOADER, documentLoader)
.build();
List<ReportItems> accumulator = new ArrayList<>();
@ -97,7 +100,7 @@ public class OB20Inspector extends Inspector {
if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
// we expect the above to place a generated object in the context
Assertion assertion = ctx.getGeneratedObject(Assertion.ID);
Assertion assertion = ctx.getGeneratedObject(resource.getID());
//context and type properties
CredentialEnum type = assertion.getCredentialType();
@ -108,11 +111,11 @@ public class OB20Inspector extends Inspector {
}
// let's compact
accumulator.add(getCompactionProbe(assertion).run(assertion, ctx));
accumulator.add(new JsonLDCompactionProve(assertion.getCredentialType().getContextUris().get(0)).run(assertion, ctx));
if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
// validate JSON LD
JsonLdGeneratedObject jsonLdGeneratedObject = ctx.getGeneratedObject(JsonLdGeneratedObject.ID);
JsonLdGeneratedObject jsonLdGeneratedObject = ctx.getGeneratedObject(JsonLDCompactionProve.getId(assertion));
accumulator.add(new JsonLDValidationProbe(jsonLdGeneratedObject).run(assertion, ctx));
if(broken(accumulator, true)) return abort(ctx, accumulator, probeCount);
@ -145,8 +148,8 @@ public class OB20Inspector extends Inspector {
return new Report(ctx, new ReportItems(accumulator), probeCount);
}
protected JsonLDCompactionProve getCompactionProbe(Assertion assertion) {
return new JsonLDCompactionProve(assertion.getCredentialType().getContextUris().get(0));
protected DocumentLoader getDocumentLoader() {
return new CachingDocumentLoader();
}
public static class Builder extends Inspector.Builder<OB20Inspector.Builder> {

View File

@ -6,7 +6,11 @@ public class JsonLdGeneratedObject extends GeneratedObject {
private String json;
public JsonLdGeneratedObject(String json) {
super(ID, GeneratedObject.Type.INTERNAL);
this(ID, json);
}
public JsonLdGeneratedObject(String id, String json) {
super(id, GeneratedObject.Type.INTERNAL);
this.json = json;
}

View File

@ -1,36 +1,29 @@
package org.oneedtech.inspect.vc.jsonld.probe;
import java.io.StringReader;
import java.net.URI;
import java.util.Map;
import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.probe.RunContext.Key;
import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.Credential;
import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import com.apicatalog.jsonld.JsonLd;
import com.apicatalog.jsonld.JsonLdOptions;
import com.apicatalog.jsonld.api.CompactionApi;
import com.apicatalog.jsonld.document.JsonDocument;
import com.apicatalog.jsonld.loader.DocumentLoader;
import jakarta.json.JsonObject;
public class JsonLDCompactionProve extends Probe<Credential> {
private final String context;
private final Map<URI, String> localDomains;
public JsonLDCompactionProve(String context) {
this(context, null);
}
public JsonLDCompactionProve(String context, Map<URI, String> localDomains) {
super(ID);
this.context = context;
this.localDomains = localDomains;
}
@Override
@ -39,10 +32,10 @@ public class JsonLDCompactionProve extends Probe<Credential> {
// compact JSON
JsonDocument jsonDocument = JsonDocument.of(new StringReader(crd.getJson().toString()));
CompactionApi compactApi = JsonLd.compact(jsonDocument, context);
compactApi.options(new JsonLdOptions(new CachingDocumentLoader(localDomains)));
compactApi.options(new JsonLdOptions((DocumentLoader) ctx.get(Key.JSON_DOCUMENT_LOADER)));
JsonObject compactedObject = compactApi.get();
ctx.addGeneratedObject(new JsonLdGeneratedObject(compactedObject.toString()));
ctx.addGeneratedObject(new JsonLdGeneratedObject(getId(crd), compactedObject.toString()));
// Handle mismatch between URL node source and declared ID.
if (compactedObject.get("id") != null && crd.getResource().getID() != null
@ -57,5 +50,9 @@ public class JsonLDCompactionProve extends Probe<Credential> {
}
}
public static String getId(Credential crd) {
return "json-ld-compact:" + crd.getResource().getID();
}
public static final String ID = JsonLDCompactionProve.class.getSimpleName();
}

View File

@ -1,27 +1,53 @@
package org.oneedtech.inspect.vc.probe;
import static org.oneedtech.inspect.vc.Assertion.ValueType.DATA_URI;
import static org.oneedtech.inspect.vc.Assertion.ValueType.DATA_URI_OR_URL;
import static org.oneedtech.inspect.vc.Assertion.ValueType.URL;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.oneedtech.inspect.core.probe.Outcome;
import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.probe.RunContext.Key;
import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.Validation;
import org.oneedtech.inspect.core.report.ReportUtil;
import org.oneedtech.inspect.util.resource.UriResource;
import org.oneedtech.inspect.vc.Assertion;
import org.oneedtech.inspect.vc.Assertion.ValueType;
import org.oneedtech.inspect.vc.Validation;
import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import foundation.identity.jsonld.ConfigurableDocumentLoader;
public class ValidationPropertyProbe extends PropertyProbe {
private final Validation validation;
private final boolean fullValidate; // TODO: fullValidate
public ValidationPropertyProbe(Validation validation) {
this(validation, false);
}
public ValidationPropertyProbe(Validation validation, boolean fullValidate) {
super(ID + "<" + validation.getName() + ">", validation.getName());
this.validation = validation;
this.fullValidate = fullValidate;
setValidations(this::validate);
}
@Override
protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
if (validation.isRequired()) {
@ -40,6 +66,8 @@ public class ValidationPropertyProbe extends PropertyProbe {
* @return validation result
*/
private ReportItems validate(JsonNode node, RunContext ctx) {
ReportItems result = new ReportItems();
// required property
if (validation.isRequired()) {
if (node.isObject()) {
@ -72,64 +100,88 @@ public class ValidationPropertyProbe extends PropertyProbe {
}
}
} 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]
for (JsonNode childNode : nodeList) {
if (childNode.isObject()) {
result = new ReportItems(List.of(result, validateExpectedTypes(childNode, ctx)));
continue;
} else if (validation.isAllowDataUri() && !DATA_URI_OR_URL.getValidationFunction().apply(childNode)){
return error("ID-type property " + validation.getName() + " had value `" + childNode.toString() + "` that isn't URI or DATA URI in " + node.toString(), ctx);
} else if (!validation.isAllowDataUri() && !ValueType.IRI.getValidationFunction().apply(childNode)) {
return error("ID-type property " + validation.getName() + " had value `" + childNode.toString() + "` where another scheme may have been expected " + node.toString(), ctx);
}
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')))
*/
// get node from context
JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(childNode.asText());
if (resolved == null) {
if (!validation.isFetch()) {
if (validation.isAllowRemoteUrl() && URL.getValidationFunction().apply(childNode)) {
continue;
}
if (validation.isAllowDataUri() && DATA_URI.getValidationFunction().apply(childNode)) {
continue;
}
return error("Node " + node.toString() + " has " + validation.getName() +" property value `" + childNode.toString() + "` that appears not to be in URI format", ctx);
} else {
// fetch
UriResource uriResource = resolveUriResource(ctx, childNode);
result = new ReportItems(List.of(result, new CredentialParseProbe().run(uriResource, ctx)));
if (!result.contains(Outcome.FATAL, Outcome.EXCEPTION)) {
Assertion assertion = (Assertion) ctx.getGeneratedObject(uriResource.getID());
// compact ld
result = new ReportItems(List.of(result, new JsonLDCompactionProve(assertion.getCredentialType().getContextUris().get(0)).run(assertion, ctx)));
if (!result.contains(Outcome.FATAL, Outcome.EXCEPTION)) {
JsonLdGeneratedObject fetched = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(assertion));
JsonNode fetchedNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER)).readTree(fetched.getJson());
// validate document
result = new ReportItems(List.of(result, validateExpectedTypes(fetchedNode, ctx)));
}
}
}
} else {
// validate expected node class
result = new ReportItems(List.of(result, validateExpectedTypes(childNode, ctx)));
}
}
}
} catch (Throwable t) {
return fatal(t.getMessage(), ctx);
}
return success(ctx);
return result.size() > 0 ? result : success(ctx);
}
public static final String ID = ValidationPropertyProbe.class.getSimpleName();
private UriResource resolveUriResource(RunContext ctx, JsonNode childNode) throws URISyntaxException {
URI uri = new URI(childNode.asText());
UriResource initialUriResource = new UriResource(uri);
UriResource uriResource = initialUriResource;
// check if uri points to a local resource
if (ctx.get(Key.JSON_DOCUMENT_LOADER) instanceof ConfigurableDocumentLoader) {
if (ConfigurableDocumentLoader.getDefaultHttpLoader() instanceof CachingDocumentLoader.HttpLoader) {
URI resolvedUri = ((CachingDocumentLoader.HttpLoader) ConfigurableDocumentLoader.getDefaultHttpLoader()).resolve(uri);
uriResource = new UriResource(resolvedUri);
}
}
return uriResource;
}
private ReportItems validateExpectedTypes(JsonNode node, RunContext ctx) {
List<ReportItems> results = validation.getExpectedTypes().stream()
.flatMap(type -> type.getValidations().stream())
.map(v -> new ValidationPropertyProbe(v, validation.isFullValidate()))
.map(probe -> {
try {
return probe.run(node, ctx);
} catch (Exception e) {
return ReportUtil.onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, null, e);
}
})
.collect(Collectors.toList());
return new ReportItems(results);
}
public static final String ID = ValidationPropertyProbe.class.getSimpleName();
}

View File

@ -80,7 +80,7 @@ public class CachingDocumentLoader extends ConfigurableDocumentLoader {
* Resolved given url. If the url is from one of local domain, a URL of the relative resource will be returned
* @throws URISyntaxException
*/
private URI resolve(URI url) throws URISyntaxException {
public URI resolve(URI url) throws URISyntaxException {
if (localDomains != null) {
URI base = url.resolve("/");
if (localDomains.containsKey(base)) {

View File

@ -110,7 +110,7 @@ public class PrimitiveValueValidator {
}
ObjectMapper mapper = new ObjectMapper(); // TODO: get from RunContext
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper);
JsonPathEvaluator jsonPath = new JsonPathEvaluator(mapper); // TODO: get from RunContext
try {
JsonNode node = mapper.readTree(Resources.getResource("contexts/ob-v2p0.json"));

View File

@ -28,6 +28,8 @@ public class OB20Tests {
static void setup() throws URISyntaxException {
validator = new TestBuilder()
.add(new URI("https://www.example.org/"), "ob20/assets")
.add(new URI("https://example.org/"), "ob20/assets")
.add(new URI("http://example.org/"), "ob20/assets")
.set(Behavior.TEST_INCLUDE_SUCCESS, true)
.set(Behavior.TEST_INCLUDE_WARNINGS, false)
.set(Behavior.VALIDATOR_FAIL_FAST, true)
@ -55,11 +57,12 @@ public class OB20Tests {
@Test
void testSimpleBadgeClassJsonValid() {
assertDoesNotThrow(()->{
Report report = validator.run(Samples.OB20.JSON.SIMPLE_BADGECLASS.asFileResource());
if(verbose) PrintHelper.print(report, true);
assertValid(report);
});
// TODO: commented out due to lack of prerequisite tasks yet
// assertDoesNotThrow(()->{
// Report report = validator.run(Samples.OB20.JSON.SIMPLE_BADGECLASS.asFileResource());
// if(verbose) PrintHelper.print(report, true);
// assertValid(report);
// });
}
@Test

View File

@ -7,9 +7,9 @@ import java.util.Map;
import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.util.spec.Specification;
import org.oneedtech.inspect.vc.Assertion;
import org.oneedtech.inspect.vc.OB20Inspector;
import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
import com.apicatalog.jsonld.loader.DocumentLoader;
/**
* OpenBadges 2.0 Test inspector.
@ -28,9 +28,9 @@ public class TestOB20Inspector extends OB20Inspector {
}
@Override
protected JsonLDCompactionProve getCompactionProbe(Assertion assertion) {
return new JsonLDCompactionProve(assertion.getCredentialType().getContextUris().get(0), localDomains);
}
protected DocumentLoader getDocumentLoader() {
return new CachingDocumentLoader(localDomains);
}
public static class TestBuilder extends OB20Inspector.Builder {
final Map<URI, String> localDomains;