diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Assertion.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Assertion.java index 7410903..1e31358 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Assertion.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Assertion.java @@ -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(), diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java index 0f6a406..0a911f7 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/OB20Inspector.java @@ -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 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)); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Validation.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Validation.java index 42a0b6f..4e2fb2e 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Validation.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/Validation.java @@ -13,7 +13,7 @@ public class Validation { private final boolean required; private final boolean many; private final List mustContainOne; - private final List prerequisites; + private final List prerequisites; private final List 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 getPrerequisites() { + public List 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 mustContainOne; - private List prerequisites; + private List prerequisites; private List 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 elems) { + public Builder prerequisites(List 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); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/JsonLdGeneratedObject.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/JsonLdGeneratedObject.java index fa7dcff..c53662c 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/JsonLdGeneratedObject.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/JsonLdGeneratedObject.java @@ -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(); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/GraphFetcherProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/GraphFetcherProbe.java new file mode 100644 index 0000000..dff44db --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/GraphFetcherProbe.java @@ -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 { + 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 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 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); +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDCompactionProve.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDCompactionProve.java index 40a90d8..92c4a51 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDCompactionProve.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/jsonld/probe/JsonLDCompactionProve.java @@ -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 { private final String context; diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationDependenciesProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationDependenciesProbe.java new file mode 100644 index 0000000..9014c75 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationDependenciesProbe.java @@ -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 { + 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 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 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(); + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationRecipientProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationRecipientProbe.java new file mode 100644 index 0000000..c6f4972 --- /dev/null +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/VerificationRecipientProbe.java @@ -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 { + 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 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 allowedTypes = List.of("id", "email", "url", "telephone"); + public static final String ID = VerificationRecipientProbe.class.getSimpleName(); + +} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationFlattenEmbeddedResourcePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationFlattenEmbeddedResourcePropertyProbe.java deleted file mode 100644 index 604f39a..0000000 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationFlattenEmbeddedResourcePropertyProbe.java +++ /dev/null @@ -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); - } - - -} diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationImagePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationImagePropertyProbe.java index d2dfa30..3dd3f2c 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationImagePropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationImagePropertyProbe.java @@ -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 allowedMimeTypes = List.of(MimeType.IMAGE_PNG, MimeType.IMAGE_SVG); + public static final String ID = ValidationImagePropertyProbe.class.getSimpleName(); + } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationIssuerPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationIssuerPropertyProbe.java index 789c750..62ab1d4 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationIssuerPropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationIssuerPropertyProbe.java @@ -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(); + } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbe.java index e6aa71c..53e0055 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationPropertyProbe.java @@ -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,35 +129,20 @@ 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; - } - - 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 - 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))); - } - } + 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 { + ObjectMapper mapper = (ObjectMapper) ctx.get(Key.JACKSON_OBJECTMAPPER); + JsonNode resolvedNode = mapper.readTree(resolved.getJson()); + // 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 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 results = validation.getExpectedTypes().stream() .flatMap(type -> type.getValidations().stream()) diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationRdfTypePropertyProbe.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationRdfTypePropertyProbe.java index 8929f79..fc9ecf1 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationRdfTypePropertyProbe.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/probe/validation/ValidationRdfTypePropertyProbe.java @@ -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(); } diff --git a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java index 630e43a..080bc6f 100644 --- a/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java +++ b/inspector-vc/src/main/java/org/oneedtech/inspect/vc/util/JsonNodeUtil.java @@ -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(); + } }