More validations and prerequisites

This commit is contained in:
Xavi Aracil 2022-12-06 15:44:15 +01:00
parent 746300ab42
commit 7580a2c56b
14 changed files with 605 additions and 150 deletions

View File

@ -171,25 +171,33 @@ public class Assertion extends Credential {
new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
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("badge").type(ValueType.ID).expectedType(Type.BadgeClass).fetch(true).required(true).allowFlattenEmbeddedResource(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(),
new Validation.Builder().name("narrative").type(ValueType.MARKDOWN_TEXT).required(false).build(),
new Validation.Builder().name("evidence").type(ValueType.ID).allowRemoteUrl(true).expectedType(Type.Evidence).many(true).fetch(false).required(false).build(),
new Validation.Builder().name("image").type(ValueType.IMAGE).required(false).many(false).allowDataUri(false).build()
new Validation.Builder().name("image").type(ValueType.IMAGE).required(false).many(false).allowDataUri(false).build(),
new Validation.Builder().name("@language").type(ValueType.LANGUAGE).required(false).build(),
new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).required(false).build(),
new Validation.Builder().name("related").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(false).allowDataUri(false).expectedType(Type.Assertion).fullValidate(false).many(true).build(),
new Validation.Builder().name("endorsement").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(true).allowDataUri(false).expectedType(Type.Endorsement).many(true).build()
))
.put(Type.BadgeClass, List.of(
new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.BadgeClass)).build(),
new Validation.Builder().name("issuer").type(ValueType.ID).prerequisite("BC_FLATTEN_ISS").expectedType(Type.Profile).fetch(true).required(true).build(),
new Validation.Builder().name("issuer").type(ValueType.ID).expectedType(Type.Profile).fetch(true).required(true).allowFlattenEmbeddedResource(true).build(),
new Validation.Builder().name("name").type(ValueType.TEXT).required(true).build(),
new Validation.Builder().name("description").type(ValueType.TEXT).required(true).build(),
new Validation.Builder().name("image").type(ValueType.ID).required(false).allowRemoteUrl(true).expectedType(Type.Image).fetch(false).allowDataUri(true).build(),
new Validation.Builder().name("criteria").type(ValueType.ID).expectedType(Type.Criteria).fetch(false).required(true).allowRemoteUrl(true).build(),
new Validation.Builder().name("alignment").type(ValueType.ID).expectedType(Type.AlignmentObject).many(true).fetch(false).required(false).build(),
new Validation.Builder().name("tags").type(ValueType.TEXT).many(true).required(false).build()
new Validation.Builder().name("tags").type(ValueType.TEXT).many(true).required(false).build(),
new Validation.Builder().name("@language").type(ValueType.LANGUAGE).required(false).build(),
new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).required(false).build(),
new Validation.Builder().name("related").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(false).allowDataUri(false).expectedType(Type.BadgeClass).fullValidate(false).many(true).build(),
new Validation.Builder().name("endorsement").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(true).allowDataUri(false).expectedType(Type.Endorsement).many(true).build()
))
.put(Type.AlignmentObject, List.of(
new Validation.Builder().name("type").type(ValueType.RDF_TYPE).many(true).required(false).defaultType(Type.AlignmentObject).build(),
@ -216,7 +224,11 @@ public class Assertion extends Credential {
new Validation.Builder().name("claim").type(ValueType.ID).required(true).allowRemoteUrl(false).fetch(false).allowDataUri(false).expectedTypes(List.of(Type.EndorsementClaim, Type.Endorsement)).fullValidate(false).build(),
new Validation.Builder().name("issuedOn").type(ValueType.DATETIME).required(true).build(),
new Validation.Builder().name("issuer").type(ValueType.ID).expectedType(Type.Profile).fetch(true).required(true).build(),
new Validation.Builder().name("verification").build()
new Validation.Builder().name("verification").build(),
new Validation.Builder().name("@language").type(ValueType.LANGUAGE).required(false).build(),
new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).required(false).build(),
new Validation.Builder().name("related").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(false).allowDataUri(false).expectedType(Type.Endorsement).fullValidate(false).many(true).build(),
new Validation.Builder().name("endorsement").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(true).allowDataUri(false).expectedType(Type.Endorsement).many(true).build()
))
.put(Type.EndorsementClaim, List.of(
new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
@ -267,7 +279,11 @@ public class Assertion extends Credential {
new Validation.Builder().name("telephone").type(ValueType.TELEPHONE).required(false).build(),
new Validation.Builder().name("publicKey").type(ValueType.ID).expectedType(Type.CryptographicKey).fetch(true).required(false).build(),
new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectIssuer).fetch(false).required(false).build(),
new Validation.Builder().name("id").type(ValueType.ISSUER).required(false).messageLevel(MessageLevel.Warning).build()
new Validation.Builder().name("id").type(ValueType.ISSUER).required(false).messageLevel(MessageLevel.Warning).build(),
new Validation.Builder().name("@language").type(ValueType.LANGUAGE).required(false).build(),
new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).required(false).build(),
new Validation.Builder().name("related").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(false).allowDataUri(false).expectedType(Type.Issuer).fullValidate(false).many(true).build(),
new Validation.Builder().name("endorsement").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(true).allowDataUri(false).expectedType(Type.Endorsement).many(true).build()
))
.put(Type.Profile, List.of(
new Validation.Builder().name("id").type(ValueType.IRI).required(true).build(),
@ -280,7 +296,11 @@ public class Assertion extends Credential {
new Validation.Builder().name("telephone").type(ValueType.TELEPHONE).required(false).build(),
new Validation.Builder().name("publicKey").type(ValueType.ID).expectedType(Type.CryptographicKey).fetch(true).required(false).build(),
new Validation.Builder().name("verification").type(ValueType.ID).expectedType(Type.VerificationObjectIssuer).fetch(false).required(false).build(),
new Validation.Builder().name("id").type(ValueType.ISSUER).required(false).messageLevel(MessageLevel.Warning).build()
new Validation.Builder().name("id").type(ValueType.ISSUER).required(false).messageLevel(MessageLevel.Warning).build(),
new Validation.Builder().name("@language").type(ValueType.LANGUAGE).required(false).build(),
new Validation.Builder().name("version").type(ValueType.TEXT_OR_NUMBER).required(false).build(),
new Validation.Builder().name("related").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(false).allowDataUri(false).expectedType(Type.Profile).fullValidate(false).many(true).build(),
new Validation.Builder().name("endorsement").type(ValueType.ID).required(false).allowRemoteUrl(true).fetch(true).allowDataUri(false).expectedType(Type.Endorsement).many(true).build()
))
.put(Type.RevocationList, List.of(
new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(true).mustContainOneType(List.of(Type.RevocationList)).build(),
@ -289,7 +309,7 @@ public class Assertion extends Credential {
.put(Type.VerificationObject, List.of())
.put(Type.VerificationObjectAssertion, List.of(
new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(true).many(false).mustContainOne(List.of("HostedBadge", "SignedBadge")).build(),
new Validation.Builder().name("creator").type(ValueType.ID).expectedType(Type.CryptographicKey).fetch(true).required(false).prerequisite("ASSERTION_VERIFICATION_DEPENDENCIES").build()
new Validation.Builder().name("creator").type(ValueType.ID).expectedType(Type.CryptographicKey).fetch(true).required(false).build()
))
.put(Type.VerificationObjectIssuer, List.of(
new Validation.Builder().name("type").type(ValueType.RDF_TYPE).required(false).many(true).defaultType(Type.VerificationObject).build(),

View File

@ -23,6 +23,7 @@ import org.oneedtech.inspect.util.resource.ResourceType;
import org.oneedtech.inspect.util.spec.Specification;
import org.oneedtech.inspect.vc.Credential.CredentialEnum;
import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
import org.oneedtech.inspect.vc.jsonld.probe.GraphFetcherProbe;
import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
import org.oneedtech.inspect.vc.jsonld.probe.JsonLDValidationProbe;
import org.oneedtech.inspect.vc.payload.PngParser;
@ -32,6 +33,7 @@ import org.oneedtech.inspect.vc.probe.CredentialParseProbe;
import org.oneedtech.inspect.vc.probe.ExpirationProbe;
import org.oneedtech.inspect.vc.probe.IssuanceProbe;
import org.oneedtech.inspect.vc.probe.TypePropertyProbe;
import org.oneedtech.inspect.vc.probe.VerificationDependenciesProbe;
import org.oneedtech.inspect.vc.probe.validation.ValidationPropertyProbeFactory;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
@ -122,12 +124,15 @@ public class OB20Inspector extends Inspector {
accumulator.add(new JsonLDValidationProbe(jsonLdGeneratedObject).run(assertion, ctx));
if(broken(accumulator, true)) 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.
// Also, we validate the Open Badge, from the compacted form
// validation the Open Badge, from the compacted form
JsonNode assertionNode = mapper.readTree(jsonLdGeneratedObject.getJson());
// mount the graph, flattening embedded resources
probeCount++;
accumulator.add(new GraphFetcherProbe(assertion).run(assertionNode, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
// perform validations
List<Validation> validations = assertion.getValidations();
for (Validation validation : validations) {
probeCount++;
@ -142,6 +147,12 @@ public class OB20Inspector extends Inspector {
accumulator.add(probe.run(assertion, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
}
// verification
probeCount++;
accumulator.add(new VerificationDependenciesProbe(assertion.getId()).run(jsonLdGeneratedObject, ctx));
if(broken(accumulator)) return abort(ctx, accumulator, probeCount);
} catch (Exception e) {
accumulator.add(onProbeException(Probe.ID.NO_UNCAUGHT_EXCEPTIONS, resource, e));
}

View File

@ -13,7 +13,7 @@ public class Validation {
private final boolean required;
private final boolean many;
private final List<String> mustContainOne;
private final List<String> prerequisites;
private final List<Validation> prerequisites;
private final List<Assertion.Type> expectedTypes;
private final boolean allowRemoteUrl;
private final boolean allowDataUri;
@ -21,6 +21,7 @@ public class Validation {
private final String defaultType;
private final boolean fullValidate;
private MessageLevel messageLevel;
private final boolean allowFlattenEmbeddedResource;
public Validation(Builder builder) {
this.name = builder.name;
@ -36,6 +37,7 @@ public class Validation {
this.defaultType = builder.defaultType;
this.fullValidate = builder.fullValidate;
this.messageLevel = builder.messageLevel;
this.allowFlattenEmbeddedResource = builder.allowFlattenEmbeddedResource;
}
public String getName() {
@ -58,7 +60,7 @@ public class Validation {
return mustContainOne;
}
public List<String> getPrerequisites() {
public List<Validation> getPrerequisites() {
return prerequisites;
}
@ -90,6 +92,10 @@ public class Validation {
return messageLevel;
}
public boolean isAllowFlattenEmbeddedResource() {
return allowFlattenEmbeddedResource;
}
public enum MessageLevel {
Warning,
Error
@ -101,7 +107,7 @@ public class Validation {
private boolean required;
private boolean many;
private List<String> mustContainOne;
private List<String> prerequisites;
private List<Validation> prerequisites;
private List<Assertion.Type> expectedTypes;
private boolean allowRemoteUrl;
private boolean allowDataUri;
@ -109,6 +115,7 @@ public class Validation {
private String defaultType;
private boolean fullValidate;
private MessageLevel messageLevel;
private boolean allowFlattenEmbeddedResource;
public Builder() {
this.mustContainOne = new ArrayList<>();
@ -147,12 +154,12 @@ public class Validation {
return this;
}
public Builder prerequisites(List<String> elems) {
public Builder prerequisites(List<Validation> elems) {
this.prerequisites = elems;
return this;
}
public Builder prerequisite(String elem) {
public Builder prerequisite(Validation elem) {
this.prerequisites = List.of(elem);
return this;
}
@ -201,6 +208,11 @@ public class Validation {
return this;
}
public Builder allowFlattenEmbeddedResource(boolean allowFlattenEmbeddedResource) {
this.allowFlattenEmbeddedResource = allowFlattenEmbeddedResource;
return this;
}
public Validation build() {
return new Validation(this);
}

View File

@ -18,5 +18,14 @@ public class JsonLdGeneratedObject extends GeneratedObject {
return json;
}
/**
* Update internal json. We allow this update because some validations updates JSON-LD id attributes with
* autogenerated ones
* @param json
*/
public void setJson(String json) {
this.json = json;
}
public static final String ID = JsonLdGeneratedObject.class.getCanonicalName();
}

View File

@ -0,0 +1,216 @@
package org.oneedtech.inspect.vc.jsonld.probe;
import static org.oneedtech.inspect.vc.Assertion.ValueType.DATA_URI_OR_URL;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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.util.resource.UriResource;
import org.oneedtech.inspect.vc.Assertion;
import org.oneedtech.inspect.vc.Assertion.Type;
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.probe.CredentialParseProbe;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import org.oneedtech.inspect.vc.util.PrimitiveValueValidator;
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 foundation.identity.jsonld.ConfigurableDocumentLoader;
/**
* Probe for fetching all elements in the graph for Open Badges 2.0 validation
* Contains the fetch part of "VALIDATE_TYPE_PROPERTY" task in python implementation, as well as the "FLATTEN_EMBEDDED_RESOURCE" task
* @author xaracil
*/
public class GraphFetcherProbe extends Probe<JsonNode> {
private final Assertion assertion;
public GraphFetcherProbe(Assertion assertion) {
super(ID);
this.assertion = assertion;
}
@Override
public ReportItems run(JsonNode root, RunContext ctx) throws Exception {
ReportItems result = new ReportItems();
// get validations of IDs and fetch
List<Validation> validations = assertion.getValidations().stream()
.filter(validation -> validation.getType() == ValueType.ID && validation.isFetch())
.collect(Collectors.toList());
for (Validation validation : validations) {
JsonNode node = root.get(validation.getName());
if (node == null) {
// if node is null, continue. ValidationPropertyProbe will check if the field was required
continue;
}
// flatten embeded resource
if (validation.isAllowFlattenEmbeddedResource()) {
if (!node.isTextual()) {
if (!node.isObject()) {
return error("Property " + validation.getName() + " referenced from " + assertion.getJson().toString() + " is not a JSON object or string as expected", ctx);
}
JsonNode idNode = node.get("id");
if (idNode == null) {
// add a new node to the graph
UUID newId = UUID.randomUUID();
JsonNode merged = createNewJson(ctx, "{\"id\": \"_:" + newId + "\"}");
ctx.addGeneratedObject(new JsonLdGeneratedObject(JsonLDCompactionProve.getId(newId.toString()), merged.toString()));
// update existing node with new id
updateNode(validation, idNode, ctx);
return warning("Node id missing at " + node.toString() + ". A blank node ID has been assigned", ctx);
} else if (!idNode.isTextual() || !PrimitiveValueValidator.validateIri(idNode)) {
return error("Embedded JSON object at " + node.asText() + " has no proper assigned id.", ctx);
} else if (assertion.getCredentialType() == Type.Assertion && !PrimitiveValueValidator.validateUrl(idNode)) {
if (!isUrn(idNode)) {
logger.info("ID format for " + idNode.toString() + " at " + assertion.getCredentialType() + " not in an expected HTTP or URN:UUID scheme");
}
// add a new node to the graph
JsonNode merged = createNewJson(ctx, node);
ctx.addGeneratedObject(new JsonLdGeneratedObject(JsonLDCompactionProve.getId(idNode.asText().strip()), merged.toString()));
// update existing node with new id
updateNode(validation, idNode, ctx);
} else {
// update existing node with new id
updateNode(validation, idNode, ctx);
// fetch node and add it to the graph
UriResource uriResource = resolveUriResource(ctx, idNode.asText().strip());
JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
if (resolved == null) {
return new CredentialParseProbe().run(uriResource, ctx);
}
}
}
}
List<JsonNode> nodeList = JsonNodeUtil.asNodeList(node);
for (JsonNode childNode : nodeList) {
if (shouldFetch(childNode, validation)) {
// get node from context
UriResource uriResource = resolveUriResource(ctx, childNode.asText());
JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
if (resolved == null) {
// fetch
result = new ReportItems(List.of(result, new CredentialParseProbe().run(uriResource, ctx)));
if (!result.contains(Outcome.FATAL, Outcome.EXCEPTION)) {
Assertion fetchedAssertion = (Assertion) ctx.getGeneratedObject(uriResource.getID());
// compact ld
result = new ReportItems(List.of(result, new JsonLDCompactionProve(fetchedAssertion.getCredentialType().getContextUris().get(0)).run(fetchedAssertion, ctx)));
if (!result.contains(Outcome.FATAL, Outcome.EXCEPTION)) {
JsonLdGeneratedObject fetched = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(fetchedAssertion));
JsonNode fetchedNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER)).readTree(fetched.getJson());
// recursive call
result = new ReportItems(List.of(result, new GraphFetcherProbe(fetchedAssertion).run(fetchedNode, ctx)));
}
}
}
}
}
}
return success(ctx);
}
/**
* Tells if we have to fetch the id. We have to fecth if:
* - the node is not a complex node
* - not (validation allow data-uri but the node is not of this type)
* - not (validation doesn't allow data-uri but the node is not an IRI)
* @param node
* @param validation
* @return
*/
private boolean shouldFetch(JsonNode node, Validation validation) {
return !node.isObject() || (!validation.isAllowDataUri() || DATA_URI_OR_URL.getValidationFunction().apply(node)) || (validation.isAllowDataUri() || ValueType.IRI.getValidationFunction().apply(node));
}
private void updateNode(Validation validation, JsonNode idNode, RunContext ctx) throws IOException {
JsonLdGeneratedObject jsonLdGeneratedObject = ctx.getGeneratedObject(JsonLDCompactionProve.getId(assertion));
JsonNode merged = createNewJson(ctx, jsonLdGeneratedObject.getJson(), "{\"" + validation.getName() + "\": \"" + idNode.asText().strip() + "\"");
jsonLdGeneratedObject.setJson(merged.toString());
}
private JsonNode createNewJson(RunContext ctx, JsonNode node) throws IOException {
return createNewJson(ctx, Resources.getResource("contexts/ob-v2p0.json"), node.toString());
}
private JsonNode createNewJson(RunContext ctx, String additional) throws IOException {
return createNewJson(ctx, Resources.getResource("contexts/ob-v2p0.json"), additional);
}
private JsonNode createNewJson(RunContext ctx, URL original, String additional) throws IOException {
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
JsonNode newNode = mapper.readTree(original);
ObjectReader readerForUpdating = mapper.readerForUpdating(newNode);
JsonNode merged = readerForUpdating.readValue(additional);
return merged;
}
private JsonNode createNewJson(RunContext ctx, String original, String updating) throws IOException {
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
JsonNode source = mapper.readTree(original);
ObjectReader readerForUpdating = mapper.readerForUpdating(source);
JsonNode merged = readerForUpdating.readValue(updating);
return merged;
}
private boolean isUrn(JsonNode idNode) {
final Pattern pattern = Pattern.compile(URN_REGEX, Pattern.CASE_INSENSITIVE);
final Matcher matcher = pattern.matcher(idNode.asText());
return matcher.matches();
}
protected UriResource resolveUriResource(RunContext ctx, String url) throws URISyntaxException {
URI uri = new URI(url);
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;
}
public static final String ID = GraphFetcherProbe.class.getSimpleName();
public static final String URN_REGEX = "^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}$'";
protected final static Logger logger = LogManager.getLogger(GraphFetcherProbe.class);
}

View File

@ -18,7 +18,11 @@ import com.apicatalog.jsonld.loader.DocumentLoader;
import jakarta.json.JsonObject;
/**
* JSON-LD compaction probe for Open Badges 2.0
* Maps to "JSONLD_COMPACT_DATA" task in python implementation
* @author xaracil
*/
public class JsonLDCompactionProve extends Probe<Credential> {
private final String context;

View File

@ -0,0 +1,137 @@
package org.oneedtech.inspect.vc.probe;
import static org.oneedtech.inspect.util.code.Defensives.checkNotNull;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
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.util.resource.UriResource;
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;
/**
* Verification probe for Open Badges 2.0
* Maps to "ASSERTION_VERIFICATION_DEPENDENCIES" task in python implementation
* @author xaracil
*/
public class VerificationDependenciesProbe extends Probe<JsonLdGeneratedObject> {
private final String assertionId;
public VerificationDependenciesProbe(String assertionId) {
super(ID);
this.assertionId = assertionId;
}
@Override
public ReportItems run(JsonLdGeneratedObject jsonLdGeneratedObject, RunContext ctx) throws Exception {
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
JsonNode jsonNode = (mapper).readTree(jsonLdGeneratedObject.getJson());
// TODO: get verification object from graph
String type = jsonNode.get("verification").get("type").asText().strip();
if ("HostedBadge".equals(type)) {
// get badge
UriResource badgeUriResource = resolveUriResource(ctx, jsonNode.get("badge").asText().strip());
JsonLdGeneratedObject badgeObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
JsonLDCompactionProve.getId(badgeUriResource));
// get issuer from badge
JsonNode badgeNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
.readTree(badgeObject.getJson());
UriResource issuerUriResource = resolveUriResource(ctx, badgeNode.get("issuer").asText().strip());
JsonLdGeneratedObject issuerObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
JsonLDCompactionProve.getId(issuerUriResource));
JsonNode issuerNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
.readTree(issuerObject.getJson());
// verify issuer
JsonNode verificationPolicy = issuerNode.get("verification");
try {
checkNotNull(verificationPolicy);
if (verificationPolicy.isTextual()) {
// get verification node
JsonLdGeneratedObject verificationPolicyObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(
JsonLDCompactionProve.getId(verificationPolicy.asText().strip()));
verificationPolicy = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER))
.readTree(verificationPolicyObject.getJson());
}
} catch (Throwable t) {
verificationPolicy = getDefaultVerificationPolicy(issuerNode, mapper);
}
// starts with check
if (verificationPolicy.has("startsWith")) {
List<String> startsWith = JsonNodeUtil.asStringList(verificationPolicy.get("startsWith"));
if (!startsWith.stream().anyMatch(assertionId::startsWith)) {
return error("Assertion id " + assertionId
+ "does not start with any permitted values in its issuer's verification policy.", ctx);
}
}
// allowed origins
JsonNode allowedOriginsNode = verificationPolicy.get("allowedOrigins");
List<String> allowedOrigins = null;
String issuerId = issuerNode.get("id").asText().strip();
if (allowedOriginsNode == null || allowedOriginsNode.isNull()) {
allowedOrigins = List.of(getDefaultAllowedOrigins(issuerId));
} else {
JsonNodeUtil.asStringList(allowedOriginsNode);
}
if (allowedOrigins == null || allowedOrigins.isEmpty() || !issuerId.startsWith("http")) {
return warning("Issuer " + issuerId + " has no HTTP domain to enforce hosted verification policy against.", ctx);
}
if (!allowedOrigins.contains(new URI(assertionId).getAuthority())) {
return error("Assertion " + assertionId + " not hosted in allowed origins " + allowedOrigins, ctx);
}
}
return success(ctx);
}
private JsonNode getDefaultVerificationPolicy(JsonNode issuerNode, ObjectMapper mapper) throws URISyntaxException {
String issuerId =issuerNode.get("id").asText().strip();
return mapper.createObjectNode()
.put("type", "VerificationObject")
.put("allowedOrigins", getDefaultAllowedOrigins(issuerId))
.put("verificationProperty", "id");
}
private String getDefaultAllowedOrigins(String issuerId) throws URISyntaxException {
URI issuerUri = new URI(issuerId);
return issuerUri.getAuthority();
}
protected UriResource resolveUriResource(RunContext ctx, String url) throws URISyntaxException {
URI uri = new URI(url);
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;
}
public static final String ID = VerificationDependenciesProbe.class.getSimpleName();
}

View File

@ -0,0 +1,95 @@
package org.oneedtech.inspect.vc.probe;
import java.util.List;
import org.bouncycastle.crypto.digests.GeneralDigest;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.util.encoders.Hex;
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.Assertion;
import org.oneedtech.inspect.vc.jsonld.JsonLdGeneratedObject;
import org.oneedtech.inspect.vc.jsonld.probe.JsonLDCompactionProve;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Recipient Verification probe for Open Badges 2.0
* Maps to "VERIFY_RECIPIENT_IDENTIFIER" task in python implementation
* @author xaracil
*/
public class VerificationRecipientProbe extends Probe<Assertion> {
final String profileId;
public VerificationRecipientProbe(String profileId) {
super(ID);
this.profileId = profileId;
}
@Override
public ReportItems run(Assertion assertion, RunContext ctx) throws Exception {
ReportItems warnings = new ReportItems();
JsonNode recipientNode = assertion.getJson().get("recipient");
JsonLdGeneratedObject profileObject = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(profileId));
JsonNode profileNode = ((ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER)).readTree(profileObject.getJson());
String type = recipientNode.get("type").asText().strip();
if (!allowedTypes.contains(type)) {
warnings = warning("Recipient identifier type " + type + " in assertion " + assertion.getJson().toString() + " is not one of the recommended types", ctx);
}
JsonNode typeNode = profileNode.get(type);
if (JsonNodeUtil.isEmpty(typeNode)) {
return new ReportItems(List.of(warnings, error("Profile identifier property of type " + typeNode + " not found in submitted profile " + profileId, ctx)));
}
JsonNode hashNode = recipientNode.get("hashed");
List<String> currentTypes = JsonNodeUtil.asStringList(typeNode);
String identity = recipientNode.get("identity").asText().strip().toLowerCase();
String confirmedId = null;
if (JsonNodeUtil.isNotEmpty(hashNode) && hashNode.asBoolean()) {
String salt = recipientNode.get("salt").asText().strip();
for (String possibleId : currentTypes) {
if (hashMatch(possibleId, identity, salt)) {
confirmedId = possibleId;
break;
}
}
if (confirmedId == null) {
return new ReportItems(List.of(warnings, error("Profile " + profileId + " identifier(s) " + currentTypes + " of type " + typeNode.toString() + " did not match assertion " + assertion.getId() + " recipient hash " + identity + ".", ctx)));
}
} else if (currentTypes.contains(identity)) {
confirmedId = identity;
} else {
return new ReportItems(List.of(warnings, error("Profile " + profileId + " identifier " + currentTypes + " of type " + typeNode.toString() + " did not match assertion " + assertion.getId() + " recipient value " + identity, ctx)));
}
return new ReportItems(List.of(warnings, success(ctx)));
}
private boolean hashMatch(String possibleId, String identity, String salt) throws Exception {
String text = possibleId + salt;
GeneralDigest digest = null;
if (identity.startsWith("md5")) {
digest = new MD5Digest();
} else if (identity.startsWith("sha256")) {
digest = new SHA256Digest();
} else {
throw new IllegalAccessException("Cannot interpret hash type of " + identity);
}
digest.update(text.getBytes(), 0, text.length());
byte[] digested = new byte[digest.getDigestSize()];
digest.doFinal(digested, 0);
return new String(Hex.encode(digested)).equals(identity);
}
private static final List<String> allowedTypes = List.of("id", "email", "url", "telephone");
public static final String ID = VerificationRecipientProbe.class.getSimpleName();
}

View File

@ -1,94 +0,0 @@
package org.oneedtech.inspect.vc.probe.validation;
import java.util.UUID;
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.util.resource.UriResource;
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.PrimitiveValueValidator;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.google.common.io.Resources;
public class ValidationFlattenEmbeddedResourcePropertyProbe extends ValidationPropertyProbe {
public ValidationFlattenEmbeddedResourcePropertyProbe(Validation validation) {
super(validation);
}
public ValidationFlattenEmbeddedResourcePropertyProbe(Validation validation, boolean fullValidate) {
super(validation, fullValidate);
}
@Override
protected ReportItems reportForNonExistentProperty(JsonNode node, RunContext ctx) {
return notRun("Expected property " + validation.getName() + " was missing in node " + node.toString(), ctx);
}
@Override
protected ReportItems validate(JsonNode node, RunContext ctx) {
try {
UriResource uriResource = resolveUriResource(ctx, node.asText());
JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
JsonNode fetchedNode = mapper.readTree(resolved.getJson());
if (fetchedNode.isTextual()) {
return notRun("Property " + validation.getName() + " referenced from " + node.toString() + " is not embedded in need of flattening", ctx);
}
if (!fetchedNode.isObject()) {
return error("Property " + validation.getName() + " referenced from " + node.toString() + " is not a JSON object or string as expected", ctx);
}
JsonNode idNode = fetchedNode.get("id");
if (idNode == null) {
// add a new node to the graph
JsonNode newNode = mapper.readTree(Resources.getResource("contexts/ob-v2p0.json"));
ObjectReader readerForUpdating = mapper.readerForUpdating(newNode);
UUID newId = UUID.randomUUID();
JsonNode merged = readerForUpdating.readValue("{\"id\": \"_:" + newId + "\"}");
ctx.addGeneratedObject(new JsonLdGeneratedObject(JsonLDCompactionProve.getId(newId.toString()), merged.toString()));
return warning("Node id missing at " + node.toString() + ". A blank node ID has been assigned", ctx);
} else if (!idNode.isTextual() && !PrimitiveValueValidator.validateIri(idNode)) {
return error("Embedded JSON object at " + node.asText() + " has no proper assigned id.", ctx);
} else if (/*node_class == Assertion && */ !PrimitiveValueValidator.validateUrl(idNode)) {
/*
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))
*/
}
} catch (Throwable t) {
return fatal(t.getMessage(), ctx);
}
return success(ctx);
}
}

View File

@ -13,14 +13,19 @@ import org.oneedtech.inspect.vc.util.PrimitiveValueValidator;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Image validation for Open Badges 2.0
* Maps to "IMAGE_VALIDATION" task in python implementation
* @author xaracil
*/
public class ValidationImagePropertyProbe extends ValidationPropertyProbe {
public ValidationImagePropertyProbe(Validation validation) {
super(validation);
super(ID, validation);
}
public ValidationImagePropertyProbe(Validation validation, boolean fullValidate) {
super(validation, fullValidate);
super(ID, validation, fullValidate);
}
@Override
@ -62,4 +67,6 @@ public class ValidationImagePropertyProbe extends ValidationPropertyProbe {
}
private static final List<MimeType> allowedMimeTypes = List.of(MimeType.IMAGE_PNG, MimeType.IMAGE_SVG);
public static final String ID = ValidationImagePropertyProbe.class.getSimpleName();
}

View File

@ -7,14 +7,19 @@ import org.oneedtech.inspect.vc.Validation.MessageLevel;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Issuer properties additional validator for Open Badges 2.0
* Maps to "ISSUER_PROPERTY_DEPENDENCIES" task in python implementation
* @author xaracil
*/
public class ValidationIssuerPropertyProbe extends ValidationPropertyProbe {
public ValidationIssuerPropertyProbe(Validation validation) {
super(validation);
super(ID, validation);
}
public ValidationIssuerPropertyProbe(Validation validation, boolean fullValidate) {
super(validation, fullValidate);
super(ID, validation, fullValidate);
}
@Override
@ -32,4 +37,7 @@ public class ValidationIssuerPropertyProbe extends ValidationPropertyProbe {
}
return error(msg, ctx);
}
public static final String ID = ValidationIssuerPropertyProbe.class.getSimpleName();
}

View File

@ -17,12 +17,10 @@ import org.oneedtech.inspect.core.probe.RunContext.Key;
import org.oneedtech.inspect.core.report.ReportItems;
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.probe.CredentialParseProbe;
import org.oneedtech.inspect.vc.probe.PropertyProbe;
import org.oneedtech.inspect.vc.util.CachingDocumentLoader;
import org.oneedtech.inspect.vc.util.JsonNodeUtil;
@ -32,17 +30,29 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import foundation.identity.jsonld.ConfigurableDocumentLoader;
/**
* Validator for properties of type other than ValueType.RDF_TYPE in Open Badges 2.0 types
* Maps to "VALIDATE_TYPE_PROPERTY" task in python implementation
* @author xaracil
*/
public class ValidationPropertyProbe extends PropertyProbe {
protected final Validation validation;
protected final boolean fullValidate; // TODO: fullValidate
public ValidationPropertyProbe(Validation validation) {
this(validation, false);
this(ID, validation, false);
}
public ValidationPropertyProbe(String id, Validation validation) {
this(ID, validation, false);
}
public ValidationPropertyProbe(Validation validation, boolean fullValidate) {
super(ID + "<" + validation.getName() + ">", validation.getName());
this(ID, validation, fullValidate);
}
public ValidationPropertyProbe(String id, Validation validation, boolean fullValidate) {
super(id + "<" + validation.getName() + ">", validation.getName());
this.validation = validation;
this.fullValidate = fullValidate;
setValidations(this::validate);
@ -100,6 +110,11 @@ public class ValidationPropertyProbe extends PropertyProbe {
}
}
} else {
// pre-requisites
result = new ReportItems(List.of(result, validatePrerequisites(node, ctx)));
if (result.contains(Outcome.ERROR, Outcome.EXCEPTION)) {
return result;
}
for (JsonNode childNode : nodeList) {
if (childNode.isObject()) {
result = new ReportItems(List.of(result, validateExpectedTypes(childNode, ctx)));
@ -114,7 +129,6 @@ public class ValidationPropertyProbe extends PropertyProbe {
UriResource uriResource = resolveUriResource(ctx, childNode.asText());
JsonLdGeneratedObject resolved = (JsonLdGeneratedObject) ctx.getGeneratedObject(JsonLDCompactionProve.getId(uriResource));
if (resolved == null) {
if (!validation.isFetch()) {
if (validation.isAllowRemoteUrl() && URL.getValidationFunction().apply(childNode)) {
continue;
}
@ -124,25 +138,11 @@ public class ValidationPropertyProbe extends PropertyProbe {
}
return error("Node " + node.toString() + " has " + validation.getName() +" property value `" + childNode.toString() + "` that appears not to be in URI format", ctx);
} else {
// fetch
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());
ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER);
JsonNode resolvedNode = mapper.readTree(resolved.getJson());
// 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)));
result = new ReportItems(List.of(result, validateExpectedTypes(resolvedNode, ctx)));
}
}
}
@ -168,6 +168,21 @@ public class ValidationPropertyProbe extends PropertyProbe {
return uriResource;
}
private ReportItems validatePrerequisites(JsonNode node, RunContext ctx) {
List<ReportItems> results = validation.getPrerequisites().stream()
.map(v -> ValidationPropertyProbeFactory.of(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);
}
private ReportItems validateExpectedTypes(JsonNode node, RunContext ctx) {
List<ReportItems> results = validation.getExpectedTypes().stream()
.flatMap(type -> type.getValidations().stream())

View File

@ -13,13 +13,18 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.TextNode;
/**
* Validator for properties of type ValueType.RDF_TYPE in Open Badges 2.0 types
* Maps to "VALIDATE_RDF_TYPE_PROPERTY" task in python implementation
* @author xaracil
*/
public class ValidationRdfTypePropertyProbe extends ValidationPropertyProbe {
public ValidationRdfTypePropertyProbe(Validation validation) {
super(validation);
super(ID, validation);
}
public ValidationRdfTypePropertyProbe(Validation validation, boolean fullValidate) {
super(validation, fullValidate);
super(ID, validation, fullValidate);
}
@Override
@ -55,4 +60,6 @@ public class ValidationRdfTypePropertyProbe extends ValidationPropertyProbe {
}
return new ReportItems(List.of(result, success(ctx)));
}
public static final String ID = ValidationRdfTypePropertyProbe.class.getSimpleName();
}

View File

@ -58,4 +58,12 @@ public class JsonNodeUtil {
.collect(Collectors.toList());
}
}
public static boolean isNotEmpty(JsonNode node) {
return node != null && !node.isNull() && !node.isEmpty();
}
public static boolean isEmpty(JsonNode node) {
return node == null || node.isNull() || node.isEmpty();
}
}