Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adds checks on prefixes post JSON-LD expansion process #4235

Merged
merged 2 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/common/lib/json-ld-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies {
api(libs.titaniumJsonLd)
implementation(libs.jackson.datatype.jsr310)

implementation(project(":core:common:lib:validator-lib"))
implementation(project(":spi:common:core-spi"))
implementation(project(":spi:common:json-ld-spi"))
testImplementation(project(":core:common:lib:util-lib"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class JsonLdConfiguration {

private boolean httpEnabled = false;
private boolean httpsEnabled = false;
private boolean checkPrefixes = true;

private JsonLdConfiguration() {

Expand All @@ -31,6 +32,10 @@ public boolean isHttpsEnabled() {
return httpsEnabled;
}

public boolean shouldCheckPrefixes() {
return checkPrefixes;
}

public static class Builder {

private final JsonLdConfiguration configuration = new JsonLdConfiguration();
Expand All @@ -49,6 +54,11 @@ public Builder httpsEnabled(boolean httpsEnabled) {
return this;
}

public Builder checkPrefixes(boolean checkPrefixes) {
configuration.checkPrefixes = checkPrefixes;
return this;
}

public JsonLdConfiguration build() {
return configuration;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.eclipse.edc.spi.constants.CoreConstants;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.validator.jsonobject.JsonObjectValidator;
import org.eclipse.edc.validator.jsonobject.validators.MissingPrefixes;

import java.net.URI;
import java.util.Collections;
Expand Down Expand Up @@ -61,13 +63,21 @@ public class TitaniumJsonLd implements JsonLd {
private final Map<String, Set<String>> scopedContexts = new HashMap<>();
private final CachedDocumentLoader documentLoader;

private final JsonObjectValidator validator;

private final boolean shouldCheckPrefixes;

public TitaniumJsonLd(Monitor monitor) {
this(monitor, JsonLdConfiguration.Builder.newInstance().build());
}

public TitaniumJsonLd(Monitor monitor, JsonLdConfiguration configuration) {
this.monitor = monitor;
this.documentLoader = new CachedDocumentLoader(configuration, monitor);
this.shouldCheckPrefixes = configuration.shouldCheckPrefixes();
this.validator = JsonObjectValidator.newValidator()
.verify((path) -> new MissingPrefixes(path, this::getAllPrefixes))
.build();
}

@Override
Expand All @@ -77,8 +87,15 @@ public Result<JsonObject> expand(JsonObject json) {
var expanded = com.apicatalog.jsonld.JsonLd.expand(document)
.options(new JsonLdOptions(documentLoader))
.get();
if (expanded.size() > 0) {
return Result.success(expanded.getJsonObject(0));
if (!expanded.isEmpty()) {
var object = expanded.getJsonObject(0);
if (shouldCheckPrefixes) {
var result = validator.validate(object);
if (result.failed()) {
return Result.failure(result.getFailureDetail());
}
}
return Result.success(object);
}
return Result.failure("Error expanding JSON-LD structure: result was empty, it could be caused by missing '@context'");
} catch (JsonLdError error) {
Expand Down Expand Up @@ -174,6 +191,13 @@ private Stream<String> contextsForScope(String scope) {
return scopedContexts.getOrDefault(scope, EMPTY_CONTEXTS).stream();
}

private Set<String> getAllPrefixes() {
return scopedNamespaces.values().stream()
.flatMap(v -> v.entrySet().stream())
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}

private static class CachedDocumentLoader implements DocumentLoader {

private final Map<String, URI> uriCache = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@ void expand_shouldFail_whenPropertiesWithoutNamespaceAndContextIsMissing() {
assertThat(expanded).isFailed();
}

@Test
void expand_shouldSucceed_whenChecksDisabledOnMissingContext() {
var jsonObject = createObjectBuilder()
.add(JsonLdKeywords.CONTEXT, createObjectBuilder().build())
.add("custom:item", "foo")
.build();
var jsonLd = defaultService(JsonLdConfiguration.Builder.newInstance().checkPrefixes(false).build());

jsonLd.registerNamespace("custom", "https://custom.namespace.org/schema/");

var expanded = jsonLd.expand(jsonObject);

assertThat(expanded).isSucceeded();
}

@Test
void expand_shouldFail_whenMissingContext() {
var jsonObject = createObjectBuilder()
.add(JsonLdKeywords.CONTEXT, createObjectBuilder().build())
.add("custom:item", "foo")
.build();
var jsonLd = defaultService();

jsonLd.registerNamespace("custom", "https://custom.namespace.org/schema/");

var expanded = jsonLd.expand(jsonObject);

assertThat(expanded).isFailed();
}

@Test
void expand_withCustomContext() {
var jsonObject = createObjectBuilder()
Expand All @@ -97,6 +127,7 @@ void expand_withCustomContext() {
.contains("@value\":\"value2\"");
}


@Test
void compact() {
var ns = "https://test.org/schema/";
Expand Down Expand Up @@ -313,6 +344,10 @@ private JsonLd httpEnabledService() {
}

private JsonLd defaultService() {
return new TitaniumJsonLd(monitor);
return defaultService(JsonLdConfiguration.Builder.newInstance().build());
}

private JsonLd defaultService(JsonLdConfiguration configuration) {
return new TitaniumJsonLd(monitor, configuration);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,25 @@ public class JsonObjectValidator implements Validator<JsonObject> {
private final JsonLdPath path;
private final JsonWalker walker;

public static JsonObjectValidator.Builder newValidator() {
return JsonObjectValidator.Builder.newInstance(path(), ROOT_OBJECT);
}

protected JsonObjectValidator(JsonLdPath path, JsonWalker walker) {
this.path = path;
this.walker = walker;
}

public static JsonObjectValidator.Builder newValidator() {
return JsonObjectValidator.Builder.newInstance(path(), ROOT_OBJECT);
}

@Override
public ValidationResult validate(JsonObject input) {
if (input == null) {
return ValidationResult.failure(Violation.violation("input json is null", path.toString()));
}

var violations = walker.extract(input, path)
return walker.extract(input, path)
.flatMap(target -> this.validators.stream().map(validator -> validator.validate(target)))
.filter(ValidationResult::failed)
.flatMap(it -> it.getFailure().getViolations().stream())
.toList();

if (violations.isEmpty()) {
return ValidationResult.success();
} else {
return ValidationResult.failure(violations);
}
.reduce(ValidationResult::merge)
.orElse(ValidationResult.success());
}

public static class Builder {
Expand Down Expand Up @@ -96,7 +89,7 @@ public Builder verify(Function<JsonLdPath, Validator<JsonObject>> provider) {
* Add a validator on a specific field.
*
* @param fieldName the name of the field to be validated.
* @param provider the validator provider.
* @param provider the validator provider.
* @return the builder.
*/
public Builder verify(String fieldName, Function<JsonLdPath, Validator<JsonObject>> provider) {
Expand All @@ -121,7 +114,7 @@ public Builder verifyId(Function<JsonLdPath, Validator<JsonString>> provider) {
* Add a validator on a specific nested object.
*
* @param fieldName the name of the nested object to be validated.
* @param provider the validator provider.
* @param provider the validator provider.
* @return the builder.
*/
public Builder verifyObject(String fieldName, UnaryOperator<JsonObjectValidator.Builder> provider) {
Expand All @@ -135,7 +128,7 @@ public Builder verifyObject(String fieldName, UnaryOperator<JsonObjectValidator.
* Add a validator on a specific nested array.
*
* @param fieldName the name of the nested array to be validated.
* @param provider the validator provider.
* @param provider the validator provider.
* @return the builder.
*/
public Builder verifyArrayItem(String fieldName, UnaryOperator<JsonObjectValidator.Builder> provider) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.validator.jsonobject.validators;

import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonString;
import jakarta.json.JsonValue;
import org.eclipse.edc.validator.jsonobject.JsonLdPath;
import org.eclipse.edc.validator.spi.ValidationResult;
import org.eclipse.edc.validator.spi.Validator;
import org.eclipse.edc.validator.spi.Violation;

import java.util.Arrays;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;

import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID;
import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE;

/**
* Verifies that after expansion all the properties are not prefixed with the configured prefixes for the runtime.
*/
public class MissingPrefixes implements Validator<JsonObject> {

private final JsonLdPath path;

private final Supplier<Set<String>> prefixesSupplier;

public MissingPrefixes(JsonLdPath path, Supplier<Set<String>> prefixesSupplier) {
this.path = path;
this.prefixesSupplier = prefixesSupplier;
}

@Override
public ValidationResult validate(JsonObject input) {
var prefixes = prefixesSupplier.get();
return Optional.ofNullable(input.getJsonArray(path.last()))
.filter(it -> !it.isEmpty())
.map(it -> it.getJsonObject(0))
.or(() -> Optional.of(input))
.map(it -> validateObject(it, path, prefixes))
.orElseGet(ValidationResult::success);
}

private ValidationResult validateObject(JsonObject input, JsonLdPath path, Set<String> prefixes) {
return input.entrySet().stream().map(entry -> validateField(entry.getKey(), entry.getValue(), path, prefixes))
.reduce(ValidationResult::merge)
.orElse(ValidationResult.success());
}

private ValidationResult validateArray(JsonArray array, JsonLdPath path, Set<String> prefixes) {
return array.stream().filter(f -> f instanceof JsonObject)
.map(JsonObject.class::cast)
.map(object -> validateObject(object, path, prefixes))
.reduce(ValidationResult::merge)
.orElse(ValidationResult.success());
}

private ValidationResult validateField(String name, JsonValue value, JsonLdPath path, Set<String> prefixes) {
return switch (name) {
case TYPE -> validateTypeValue(value, path, prefixes);
case ID -> validateIdValue(value, path, prefixes);
default -> validateGenericField(name, value, path, prefixes);
};

}

private ValidationResult validateTypeValue(JsonValue value, JsonLdPath path, Set<String> prefixes) {
if (value instanceof JsonArray array) {
return array.stream()
.filter(it -> it.getValueType() == JsonValue.ValueType.STRING)
.map(JsonString.class::cast)
.map(JsonString::getString)
.map(type -> validateType(type, path, prefixes))
.reduce(ValidationResult::merge)
.orElseGet(ValidationResult::success);
} else if (value instanceof JsonString type) {
return validateType(type.getString(), path, prefixes);
} else {
return ValidationResult.success();
}
}

private ValidationResult validateType(String type, JsonLdPath path, Set<String> prefixes) {
var msg = "Value of @type contains a prefix '%s' which was not expended correctly. Ensure to attach the namespace definition in the input JSON-LD.";
return validate(type, path, prefixes, msg::formatted);
}

private ValidationResult validate(String input, JsonLdPath path, Set<String> prefixes, Function<String, String> formatter) {
return Arrays.stream(input.split(":"))
.findFirst()
.map(prefix -> validatePrefix(prefix, path, prefixes, formatter))
.orElseGet(ValidationResult::success);
}

private ValidationResult validatePrefix(String prefix, JsonLdPath path, Set<String> prefixes, Function<String, String> formatter) {
if (prefixes.contains(prefix)) {
var msg = formatter.apply(prefix);
return ValidationResult.failure(Violation.violation(msg, path.toString()));
} else {
return ValidationResult.success();
}
}

private ValidationResult validateId(String id, JsonLdPath path, Set<String> prefixes) {
var msg = "Value of @id contains a prefix '%s' which was not expended correctly. Ensure to attach the namespace definition in the input JSON-LD.";
return validate(id, path, prefixes, msg::formatted);
}

private ValidationResult validateIdValue(JsonValue value, JsonLdPath path, Set<String> prefixes) {
if (value instanceof JsonString id) {
return validateId(id.getString(), path, prefixes);
} else {
return ValidationResult.success();
}
}

private ValidationResult validateGenericField(String name, JsonValue value, JsonLdPath path, Set<String> prefixes) {
var newPath = path.append(name);
var msg = "Property %s, contains a prefix '%s' which was not expended correctly. Ensure to attach the namespace definition in the input JSON-LD.";
var result = validate(name, newPath, prefixes, (prefix) -> msg.formatted(name, prefix));

if (result.failed()) {
return result;
} else {
if (value instanceof JsonObject object) {
return validateObject(object, newPath, prefixes);
} else if (value instanceof JsonArray array) {
return validateArray(array, newPath, prefixes);
} else {
return ValidationResult.success();
}
}
}
}
Loading
Loading