initial extension probe, only checks contexts

This commit is contained in:
Xavi Aracil 2022-12-14 16:12:49 +01:00
parent 727bd8194b
commit 1818fdabf5
8 changed files with 245 additions and 37 deletions

View File

@ -1,6 +1,7 @@
package org.oneedtech.inspect.vc; package org.oneedtech.inspect.vc;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static java.util.stream.Collectors.toList;
import static org.oneedtech.inspect.core.Inspector.Behavior.RESET_CACHES_ON_RUN; import static org.oneedtech.inspect.core.Inspector.Behavior.RESET_CACHES_ON_RUN;
import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException; import static org.oneedtech.inspect.core.report.ReportUtil.onProbeException;
import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT; import static org.oneedtech.inspect.util.json.ObjectMapperCache.Config.DEFAULT;
@ -13,9 +14,6 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.oneedtech.inspect.core.Inspector; import org.oneedtech.inspect.core.Inspector;
import org.oneedtech.inspect.core.probe.GeneratedObject; import org.oneedtech.inspect.core.probe.GeneratedObject;
@ -26,6 +24,7 @@ import org.oneedtech.inspect.core.probe.json.JsonPathEvaluator;
import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.Report;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.schema.JsonSchemaCache; import org.oneedtech.inspect.schema.JsonSchemaCache;
import org.oneedtech.inspect.util.code.Tuple;
import org.oneedtech.inspect.util.json.ObjectMapperCache; import org.oneedtech.inspect.util.json.ObjectMapperCache;
import org.oneedtech.inspect.util.resource.Resource; import org.oneedtech.inspect.util.resource.Resource;
import org.oneedtech.inspect.util.resource.ResourceType; import org.oneedtech.inspect.util.resource.ResourceType;
@ -166,7 +165,7 @@ public class OB20Inspector extends VCInspector {
if(broken(accumulator)) return abort(ctx, accumulator, probeCount); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
} }
// extension validations // get all json-ld generated objects for both extension and endorsements validation
List<JsonNode> jsonLdGeneratedObjects = ctx.getGeneratedObjects().values().stream() List<JsonNode> jsonLdGeneratedObjects = ctx.getGeneratedObjects().values().stream()
.filter(generatedObject -> generatedObject instanceof JsonLdGeneratedObject) .filter(generatedObject -> generatedObject instanceof JsonLdGeneratedObject)
.map(obj -> { .map(obj -> {
@ -177,10 +176,15 @@ public class OB20Inspector extends VCInspector {
throw new IllegalArgumentException("Couldn't not parse " + obj.getId() + ": contains invalid JSON"); throw new IllegalArgumentException("Couldn't not parse " + obj.getId() + ": contains invalid JSON");
} }
}) })
.collect(Collectors.toList()); .collect(toList());
for (JsonNode generatedObject : jsonLdGeneratedObjects) {
// validate extensions
List<Tuple<ExtensionProbe, JsonNode>> extensionProbeTuples = jsonLdGeneratedObjects.stream()
.flatMap(node -> getExtensionProbes(node, "id").stream())
.collect(toList());
for (Tuple<ExtensionProbe, JsonNode> extensionProbeTuple : extensionProbeTuples) {
probeCount++; probeCount++;
accumulator.add(new ExtensionProbe().run(generatedObject, ctx)); accumulator.add(extensionProbeTuple.t1.run(extensionProbeTuple.t2, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount); if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
} }
@ -192,7 +196,7 @@ public class OB20Inspector extends VCInspector {
// return endorsement node, filtering out the on inside @context // return endorsement node, filtering out the on inside @context
return asNodeList(node, "$..endorsement", jsonPath).stream().filter(endorsementNode -> !endorsementNode.isObject()); return asNodeList(node, "$..endorsement", jsonPath).stream().filter(endorsementNode -> !endorsementNode.isObject());
}) })
.collect(Collectors.toList()); .collect(toList());
for(JsonNode node : endorsements) { for(JsonNode node : endorsements) {
probeCount++; probeCount++;

View File

@ -1,8 +1,16 @@
package org.oneedtech.inspect.vc; package org.oneedtech.inspect.vc;
import static java.util.stream.Collectors.toList;
import static org.oneedtech.inspect.vc.util.JsonNodeUtil.asStringList;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.Spliterators;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.oneedtech.inspect.core.Inspector; import org.oneedtech.inspect.core.Inspector;
import org.oneedtech.inspect.core.probe.Outcome; import org.oneedtech.inspect.core.probe.Outcome;
@ -10,7 +18,10 @@ import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.report.Report; import org.oneedtech.inspect.core.report.Report;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.util.code.Tuple;
import org.oneedtech.inspect.vc.jsonld.probe.ExtensionProbe;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import com.apicatalog.jsonld.loader.DocumentLoader; import com.apicatalog.jsonld.loader.DocumentLoader;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@ -71,6 +82,46 @@ public abstract class VCInspector extends Inspector {
return new CachingDocumentLoader(); return new CachingDocumentLoader();
} }
protected List<Tuple<ExtensionProbe, JsonNode>> getExtensionProbes(JsonNode node, String entryPath) {
List<Tuple<ExtensionProbe, JsonNode>> probes = new ArrayList<>();
if (!node.isObject()) {
return probes;
}
if (node.has("type")) {
List<String> types = asStringList(node.get("type"));
// only validate extension types
if (types.contains("Extension")) {
List<String> typesToTest = types.stream().filter(type -> !type.equals("Extension")).collect(toList());
// add an extension Probe
probes.add(new Tuple<ExtensionProbe,JsonNode>(new ExtensionProbe(entryPath, typesToTest), node));
}
}
probes.addAll(StreamSupport
.stream(Spliterators.spliteratorUnknownSize(node.fields(), 0), false)
.filter(e -> !e.getKey().equals("id") && !e.getKey().equals("type"))
.flatMap(entry -> {
if (entry.getValue().isArray()) {
// recursive call
List<JsonNode> childNodes = JsonNodeUtil.asNodeList(entry.getValue());
List<Tuple<ExtensionProbe, JsonNode>> subProbes = new ArrayList<>();
for (int i = 0; i < childNodes.size(); i++) {
JsonNode childNode = childNodes.get(i);
subProbes.addAll(getExtensionProbes(childNode, entryPath + "." + entry.getKey() + "[" + i + "]"));
}
return subProbes.stream();
} else {
return getExtensionProbes(entry.getValue(), entryPath + "." + entry.getKey()).stream();
}
})
.collect(Collectors.toList())
);
return probes;
}
protected static final String REFRESHED = "is.refreshed.credential"; protected static final String REFRESHED = "is.refreshed.credential";
public abstract static class Builder<B extends VCInspector.Builder<B>> extends Inspector.Builder<B> { public abstract static class Builder<B extends VCInspector.Builder<B>> extends Inspector.Builder<B> {

View File

@ -1,37 +1,169 @@
package org.oneedtech.inspect.vc.jsonld.probe; package org.oneedtech.inspect.vc.jsonld.probe;
import static java.util.stream.Collectors.joining;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI; import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.oneedtech.inspect.core.probe.Probe; import org.oneedtech.inspect.core.probe.Probe;
import org.oneedtech.inspect.core.probe.RunContext; import org.oneedtech.inspect.core.probe.RunContext;
import org.oneedtech.inspect.core.probe.RunContext.Key; import org.oneedtech.inspect.core.probe.RunContext.Key;
import org.oneedtech.inspect.core.probe.json.JsonSchemaProbe;
import org.oneedtech.inspect.core.report.ReportItems; import org.oneedtech.inspect.core.report.ReportItems;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader; import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import com.apicatalog.jsonld.JsonLd;
import com.apicatalog.jsonld.JsonLdError;
import com.apicatalog.jsonld.JsonLdOptions;
import com.apicatalog.jsonld.document.Document;
import com.apicatalog.jsonld.document.JsonDocument;
import com.apicatalog.jsonld.loader.DocumentLoader;
import com.apicatalog.jsonld.loader.DocumentLoaderOptions;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.util.TokenBuffer;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion.VersionFlag;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObject;
import jakarta.json.JsonValue;
/**
* Probe for extensions in OB 2.0
* Maps to task "VALIDATE_EXTENSION_NODE" in python implementation
* @author xaracil
*/
public class ExtensionProbe extends Probe<JsonNode> { public class ExtensionProbe extends Probe<JsonNode> {
private final List<String> typesToTest;
public ExtensionProbe(String entryPath, List<String> typesToTest) {
super(ID, entryPath, typesToTest.stream().collect(joining()));
this.typesToTest = typesToTest;
}
@Override @Override
public ReportItems run(JsonNode node, RunContext ctx) throws Exception { public ReportItems run(JsonNode node, RunContext ctx) throws Exception {
if (!node.isObject()) { ReportItems reportItems = new ReportItems();
return success(ctx); DocumentLoader documentLoader = (DocumentLoader) ctx.get(Key.JSON_DOCUMENT_LOADER);
} Set<URI> contexts = null;
Object documentLoader = ctx.get(Key.JSON_DOCUMENT_LOADER);
Set<URI> contexts;
if (documentLoader instanceof CachingDocumentLoader) { if (documentLoader instanceof CachingDocumentLoader) {
contexts = ((CachingDocumentLoader) documentLoader).getContexts(); contexts = new HashSet<>(((CachingDocumentLoader) documentLoader).getContexts());
} else { } else {
contexts = Set.of(); contexts = Set.of();
} }
// TODO Auto-generated method stub // compact contexts
return null; URI ob20contextUri = new URI(CONTEXT_URI_STRING);
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
for (URI uri : contexts) {
if (!uri.equals(ob20contextUri)) {
JsonLdOptions options = new JsonLdOptions(documentLoader);
Document contextDocument = documentLoader.loadDocument(uri, new DocumentLoaderOptions());
JsonNode contextJson = mapper.readTree(contextDocument.getJsonContent().orElseThrow().toString());
JsonObject compactedContext = JsonLd.compact(uri, "https://w3id.org/openbadges/v2")
.options(options)
.get();
JsonNode context = mapper.readTree(compactedContext.toString());
List<JsonNode> validations = JsonNodeUtil.asNodeList(context.get("validation"));
for (JsonNode validation : validations) {
if (isLdTermInList(validation.get("validatesType"), options)) {
JsonNode schemaJson = null;
URI schemaUri = null;
try {
schemaUri = new URI(validation.get("validationSchema").asText().strip());
// check schema is valid
Document schemaDocument = documentLoader.loadDocument(schemaUri, new DocumentLoaderOptions());
schemaJson = mapper.readTree(schemaDocument.getJsonContent().orElseThrow().toString());
} catch (Exception e) {
return fatal("Could not load JSON-schema from URL " + schemaUri, ctx);
} }
private void getValidations(JsonNode node, String entryPath, Set<URI> contexts) { reportItems = new ReportItems(List.of(reportItems, validateSingleExtension(node, uri, contextJson, validation.get("validatesType").asText().strip(), schemaJson, schemaUri, options, ctx)));
}
}
}
}
if (reportItems.size() == 0) {
return error("Could not determine extension type to test", ctx);
}
return reportItems;
}
private boolean isLdTermInList(JsonNode termNode, JsonLdOptions options) throws JsonLdError {
JsonDocument jsonDocument = JsonDocument.of(Json.createObjectBuilder()
.add("@context", CONTEXT_URI_STRING)
.add("_:term", Json.createObjectBuilder()
.add("@type", termNode.asText().strip()))
.add("_:list", Json.createObjectBuilder()
.add("@type", Json.createArrayBuilder(typesToTest)))
.build());
JsonArray expandedDocument = JsonLd.expand(jsonDocument)
.options(options)
.get();
JsonArray list = expandedDocument.getJsonObject(0).getJsonArray("_:list").getJsonObject(0).getJsonArray("@type");
JsonValue term = expandedDocument.getJsonObject(0).getJsonArray("_:term").getJsonObject(0).getJsonArray("@type").get(0);
return list.contains(term);
}
private ReportItems validateSingleExtension(JsonNode node, URI uri, JsonNode context, String string, JsonNode schemaJson, URI schemaUri, JsonLdOptions options, RunContext ctx) throws JsonGenerationException, JsonMappingException, IOException, JsonLdError {
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
// validate against JSON schema, using a copy of the node
TokenBuffer tb = new TokenBuffer(mapper, false);
mapper.writeValue(tb, node);
JsonNode auxNode = mapper.readTree(tb.asParser());
ObjectReader readerForUpdating = mapper.readerForUpdating(auxNode);
JsonNode merged = readerForUpdating.readValue("{\"@context\": \"" + CONTEXT_URI_STRING + "\"}");
// combine contexts
JsonDocument contextsDocument = combineContexts(context);
JsonObject compactedObject = JsonLd.compact(JsonDocument.of(new StringReader(merged.toString())), contextsDocument)
.options(options)
.get();
// schema probe on compactedObject and schema
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V4);
JsonSchema schema = factory.getSchema(schemaUri, schemaJson);
return new JsonSchemaProbe(schema).run(mapper.readTree(compactedObject.toString()), ctx);
}
private JsonDocument combineContexts(JsonNode context) {
List<JsonNode> contexts = JsonNodeUtil.asNodeList(context);
JsonArrayBuilder contextArrayBuilder = Json.createArrayBuilder();
contextArrayBuilder.add(CONTEXT_URI_STRING); // add OB context to the list
for (JsonNode contextNode : contexts) {
if (contextNode.isTextual()) {
contextArrayBuilder.add(contextNode.asText().strip());
} else if (contextNode.isObject() && contextNode.hasNonNull("@context")) {
contextArrayBuilder.add(Json.createReader(new StringReader(contextNode.get("@context").toString())).readObject());
} }
} }
JsonDocument contextsDocument = JsonDocument.of(Json.createObjectBuilder()
.add("@context", contextArrayBuilder.build())
.build());
return contextsDocument;
}
public static final String ID = ExtensionProbe.class.getSimpleName();
private static final String CONTEXT_URI_STRING = "https://w3id.org/openbadges/v2";
}

View File

@ -36,10 +36,10 @@ public class JsonLDCompactionProve extends Probe<Credential> {
try { try {
// compact JSON // compact JSON
JsonDocument jsonDocument = JsonDocument.of(new StringReader(crd.getJson().toString())); JsonDocument jsonDocument = JsonDocument.of(new StringReader(crd.getJson().toString()));
CompactionApi compactApi = JsonLd.compact(jsonDocument, context); JsonObject compactedObject = JsonLd.compact(jsonDocument, context)
compactApi.options(new JsonLdOptions((DocumentLoader) ctx.get(Key.JSON_DOCUMENT_LOADER))); .options(new JsonLdOptions((DocumentLoader) ctx.get(Key.JSON_DOCUMENT_LOADER)))
.get();
JsonObject compactedObject = compactApi.get();
ctx.addGeneratedObject(new JsonLdGeneratedObject(getId(crd), compactedObject.toString())); ctx.addGeneratedObject(new JsonLdGeneratedObject(getId(crd), compactedObject.toString()));
// Handle mismatch between URL node source and declared ID. // Handle mismatch between URL node source and declared ID.

View File

@ -126,6 +126,8 @@ public class CachingDocumentLoader extends ConfigurableDocumentLoader {
.put("https://w3id.org/security/suites/jws-2020/v1", Resources.getResource("contexts/suites-jws-2020.jsonld")) .put("https://w3id.org/security/suites/jws-2020/v1", Resources.getResource("contexts/suites-jws-2020.jsonld"))
.put("https://openbadgespec.org/v2/context.json", Resources.getResource("contexts/ob-v2p0.json")) .put("https://openbadgespec.org/v2/context.json", Resources.getResource("contexts/ob-v2p0.json"))
.put("https://w3id.org/openbadges/v2", Resources.getResource("contexts/obv2x.jsonld")) .put("https://w3id.org/openbadges/v2", Resources.getResource("contexts/obv2x.jsonld"))
.put("https://w3id.org/openbadges/extensions/exampleExtension/context.json", Resources.getResource("contexts/obv2x-extensions.json"))
.put("https://openbadgespec.org/extensions/exampleExtension/schema.json", Resources.getResource("catalog/openbadgespec.org/extensions/exampleExtension/schema.json"))
.build(); .build();
@ -133,6 +135,7 @@ public class CachingDocumentLoader extends ConfigurableDocumentLoader {
.initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24)) .initialCapacity(32).maximumSize(64).expireAfterAccess(Duration.ofHours(24))
.build(new CacheLoader<Tuple<String, DocumentLoaderOptions>, Document>() { .build(new CacheLoader<Tuple<String, DocumentLoaderOptions>, Document>() {
public Document load(final Tuple<String, DocumentLoaderOptions> id) throws Exception { public Document load(final Tuple<String, DocumentLoaderOptions> id) throws Exception {
System.out.println("CachingDocumentLoader " + id.t1 + ": " + bundled.containsKey(id.t1));
try (InputStream is = bundled.containsKey(id.t1) try (InputStream is = bundled.containsKey(id.t1)
? bundled.get(id.t1).openStream() ? bundled.get(id.t1).openStream()
: new URI(id.t1).toURL().openStream();) { : new URI(id.t1).toURL().openStream();) {

View File

@ -0,0 +1,12 @@
{
"@context": {
"obi": "https://w3id.org/openbadges#",
"exampleProperty": "http://schema.org/text"
},
"obi:validation": [
{
"obi:validatesType": "obi:extensions/#ExampleExtension",
"obi:validationSchema": "https://openbadgespec.org/extensions/exampleExtension/schema.json"
}
]
}

View File

@ -1,5 +1,8 @@
{ {
"@context": ["https://w3id.org/openbadges/v2","https://w3id.org/openbadges/extensions/exampleExtension/context.json"], "@context": [
"https://w3id.org/openbadges/v2",
"https://w3id.org/openbadges/extensions/exampleExtension/context.json"
],
"id": "http://example.org/assertion", "id": "http://example.org/assertion",
"type": "Assertion", "type": "Assertion",
"recipient": { "recipient": {
@ -9,21 +12,21 @@
"identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942" "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
}, },
"image": "https://example.org/beths-robot-badge.png", "image": "https://example.org/beths-robot-badge.png",
"issuedOn": "2016-12-31T23:59:59Z",
"badge": "https://example.org/robotics-badge.json", "badge": "https://example.org/robotics-badge.json",
"issuedOn": "2016-12-31T23:59:59Z",
"verification": { "verification": {
"type": "hosted" "type": "hosted"
}, },
"evidence": {
"id": "_:b1",
"narrative": "Rocked the free world"
},
"extensions:exampleExtension": { "extensions:exampleExtension": {
"id": "_:b0", "id": "_:b0",
"type": [ "type": [
"Extension", "Extension",
"extensions:ExampleExtension" "obi:extensions/#ExampleExtension"
], ],
"schema:text": "I'm a property, short and sweet" "http://schema.org/text": "I'm a property, short and sweet"
},
"evidence": {
"id": "_:b1",
"narrative": "Rocked the free world"
} }
} }

View File

@ -1,5 +1,8 @@
{ {
"@context": ["https://w3id.org/openbadges/v2","https://w3id.org/openbadges/extensions/exampleExtension/context.json"], "@context": [
"https://w3id.org/openbadges/v2",
"https://w3id.org/openbadges/extensions/exampleExtension/context.json"
],
"id": "http://example.org/assertion", "id": "http://example.org/assertion",
"type": "Assertion", "type": "Assertion",
"recipient": { "recipient": {
@ -9,21 +12,21 @@
"identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942" "identity": "sha256$ecf5409f3f4b91ab60cc5ef4c02aef7032354375e70cf4d8e43f6a1d29891942"
}, },
"image": "https://example.org/beths-robot-badge.png", "image": "https://example.org/beths-robot-badge.png",
"issuedOn": "2016-12-31T23:59:59Z",
"badge": "https://example.org/robotics-badge.json", "badge": "https://example.org/robotics-badge.json",
"issuedOn": "2016-12-31T23:59:59Z",
"verification": { "verification": {
"type": "hosted" "type": "hosted"
}, },
"evidence": {
"id": "_:b1",
"narrative": "Rocked the free world"
},
"extensions:exampleExtension": { "extensions:exampleExtension": {
"id": "_:b0", "id": "_:b0",
"type": [ "type": [
"Extension", "Extension",
"extensions:ExampleExtension" "obi:extensions/#ExampleExtension"
], ],
"schema:text": 1337 "http://schema.org/text": 1337
},
"evidence": {
"id": "_:b1",
"narrative": "Rocked the free world"
} }
} }