diff --git a/deployment/helm/ditto/Chart.yaml b/deployment/helm/ditto/Chart.yaml index a720e4d9d6..dca6acdd67 100644 --- a/deployment/helm/ditto/Chart.yaml +++ b/deployment/helm/ditto/Chart.yaml @@ -16,7 +16,7 @@ description: | A digital twin is a virtual, cloud based, representation of his real world counterpart (real world “Things”, e.g. devices like sensors, smart heating, connected cars, smart grids, EV charging stations etc). type: application -version: 3.5.12 # chart version is effectively set by release-job +version: 3.5.13 # chart version is effectively set by release-job appVersion: 3.5.12 keywords: - iot-chart diff --git a/deployment/helm/ditto/templates/things-deployment.yaml b/deployment/helm/ditto/templates/things-deployment.yaml index 079113b84e..6f50866fd0 100644 --- a/deployment/helm/ditto/templates/things-deployment.yaml +++ b/deployment/helm/ditto/templates/things-deployment.yaml @@ -308,6 +308,8 @@ spec: value: "{{ .Values.things.config.wot.tdBasePrefix }}" - name: THINGS_WOT_TM_MODEL_VALIDATION_ENABLED value: "{{ .Values.things.config.wot.tmValidation.enabled }}" + - name: THINGS_WOT_TM_MODEL_VALIDATION_LOG_WARNING_INSTEAD_OF_FAILING_API_CALLS + value: "{{ .Values.things.config.wot.tmValidation.enabled }}" - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_TD_MODIFICATION value: "{{ index .Values.things.config.wot.tmValidation.thing.enforce "thing-description-modification" }}" - name: THINGS_WOT_TM_MODEL_VALIDATION_THING_ENFORCE_ATTRIBUTES diff --git a/deployment/helm/ditto/values.yaml b/deployment/helm/ditto/values.yaml index 31a53b3a7c..96b1760a8e 100644 --- a/deployment/helm/ditto/values.yaml +++ b/deployment/helm/ditto/values.yaml @@ -1045,6 +1045,8 @@ things: tmValidation: # enabled whether the ThingModel validation of Things/Features should be enabled enabled: true + # log-warning-instead-of-failing-api-calls whether to instead of to reject/fail API calls (when enabled=true), log a WARNING log instead + log-warning-instead-of-failing-api-calls: false # thing provides configuration settings for WoT based validation of Things thing: # enforce holds all configuration relating to enforcing the model @@ -1116,6 +1118,7 @@ things: # configOverrides: # # exactly the same config keys and structure applies as in the static config # enabled: true + # log-warning-instead-of-failing-api-calls: true # thing: # enforce: # thing-description-modification: false diff --git a/things/service/src/main/resources/things.conf b/things/service/src/main/resources/things.conf index e31fbc9324..e1ff362256 100755 --- a/things/service/src/main/resources/things.conf +++ b/things/service/src/main/resources/things.conf @@ -281,6 +281,10 @@ ditto { enabled = true enabled = ${?THINGS_WOT_TM_MODEL_VALIDATION_ENABLED} + # whether to instead of to reject/fail API calls (when enabled=true), log a WARNING log instead + log-warning-instead-of-failing-api-calls = false + log-warning-instead-of-failing-api-calls = ${?THINGS_WOT_TM_MODEL_VALIDATION_LOG_WARNING_INSTEAD_OF_FAILING_API_CALLS} + thing { enforce { # whether to enforce a thing whenever the "definition" of the thing is updated to a new/other WoT TM @@ -382,6 +386,7 @@ ditto { // // if the validation-context "matches" a processed API call, apply the following overrides: // config-overrides { // enabled = true +// log-warning-instead-of-failing-api-calls = true // thing { // enforce.attributes = false // } diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmValidationConfig.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmValidationConfig.java index 372f28ee84..042cf26bcc 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmValidationConfig.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/config/DefaultTmValidationConfig.java @@ -43,6 +43,7 @@ final class DefaultTmValidationConfig implements TmValidationConfig { private final List dynamicTmValidationConfigurations; private final boolean enabled; + private final boolean logWarningInsteadOfFailingApiCalls; private final ThingValidationConfig thingValidationConfig; private final FeatureValidationConfig featureValidationConfig; @@ -59,6 +60,7 @@ private DefaultTmValidationConfig(final ScopedConfig scopedConfig, .reduce(ConfigFactory.empty(), (a, b) -> b.withFallback(a)) .withFallback(scopedConfig.resolve()); enabled = effectiveConfig.getBoolean(ConfigValue.ENABLED.getConfigPath()); + logWarningInsteadOfFailingApiCalls = effectiveConfig.getBoolean(ConfigValue.LOG_WARNING_INSTEAD_OF_FAILING_API_CALLS.getConfigPath()); thingValidationConfig = DefaultThingValidationConfig.of(effectiveConfig); featureValidationConfig = DefaultFeatureValidationConfig.of(effectiveConfig); @@ -87,6 +89,11 @@ public boolean isEnabled() { return enabled; } + @Override + public boolean logWarningInsteadOfFailingApiCalls() { + return logWarningInsteadOfFailingApiCalls; + } + @Override public ThingValidationConfig getThingValidationConfig() { return thingValidationConfig; @@ -117,6 +124,7 @@ public boolean equals(final Object o) { final DefaultTmValidationConfig that = (DefaultTmValidationConfig) o; return Objects.equals(dynamicTmValidationConfigurations, that.dynamicTmValidationConfigurations) && enabled == that.enabled && + logWarningInsteadOfFailingApiCalls == that.logWarningInsteadOfFailingApiCalls && Objects.equals(thingValidationConfig, that.thingValidationConfig) && Objects.equals(featureValidationConfig, that.featureValidationConfig) && Objects.equals(scopedConfig, that.scopedConfig); @@ -124,8 +132,8 @@ public boolean equals(final Object o) { @Override public int hashCode() { - return Objects.hash(dynamicTmValidationConfigurations, enabled, thingValidationConfig, featureValidationConfig, - scopedConfig); + return Objects.hash(dynamicTmValidationConfigurations, enabled, logWarningInsteadOfFailingApiCalls, + thingValidationConfig, featureValidationConfig, scopedConfig); } @Override @@ -133,6 +141,7 @@ public String toString() { return getClass().getSimpleName() + " [" + "dynamicTmValidationConfiguration=" + dynamicTmValidationConfigurations + ", enabled=" + enabled + + ", logWarningInsteadOfFailingApiCalls=" + logWarningInsteadOfFailingApiCalls + ", thingValidationConfig=" + thingValidationConfig + ", featureValidationConfig=" + featureValidationConfig + ", scopedConfig=" + scopedConfig + diff --git a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java index d21f233a2d..56aec9c5b5 100644 --- a/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java +++ b/wot/api/src/main/java/org/eclipse/ditto/wot/api/validator/DefaultWotThingModelValidator.java @@ -19,8 +19,10 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -47,6 +49,9 @@ import org.eclipse.ditto.wot.validation.WotThingModelPayloadValidationException; import org.eclipse.ditto.wot.validation.WotThingModelValidation; import org.eclipse.ditto.wot.validation.config.TmValidationConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; /** * Default Ditto specific implementation of {@link WotThingModelValidator}. @@ -54,6 +59,7 @@ @Immutable final class DefaultWotThingModelValidator implements WotThingModelValidator { + private static final Logger log = LoggerFactory.getLogger(DefaultWotThingModelValidator.class); private final WotConfig wotConfig; private final WotThingModelResolver thingModelResolver; private final Executor executor; @@ -87,7 +93,7 @@ public CompletionStage validateThing(@Nullable final ThingDefinition thing .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> doValidateThing(Optional.ofNullable(thingDefinition).orElseThrow(), thingModel, thing, resourcePath, context, validationConfig - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThing")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -103,6 +109,7 @@ public CompletionStage validateThing(final ThingDefinition thingDefinition return provideValidationConfigIfWotValidationEnabled(context) .map(validationConfig -> doValidateThing(thingDefinition, thingModel, thing, resourcePath, context, validationConfig) + .handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThing")) ) .orElseGet(DefaultWotThingModelValidator::success); } @@ -119,7 +126,8 @@ public CompletionStage validateThingDefinitionModification(final ThingDefi ) .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> doValidateThing(thingDefinition, thingModel, thing, Thing.JsonFields.DEFINITION.getPointer(), - context, validationConfig) + context, validationConfig + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingDefinitionModification")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -154,6 +162,7 @@ public CompletionStage validateThingAttributes(@Nullable final ThingDefini return provideValidationConfigIfWotValidationEnabled(context) .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> doValidateThingAttributes(thingModel, attributes, resourcePath, context, validationConfig) + .handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingAttributes")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -169,6 +178,7 @@ public CompletionStage validateThingAttributes(final ThingDefinition thing return provideValidationConfigIfWotValidationEnabled(context) .map(validationConfig -> doValidateThingAttributes(thingModel, attributes, resourcePath, context, validationConfig) + .handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingAttributes")) ) .orElseGet(DefaultWotThingModelValidator::success); } @@ -186,7 +196,7 @@ public CompletionStage validateThingAttribute(@Nullable final ThingDefinit selectValidation(validationConfig) .validateThingAttribute(thingModel, attributePointer, attributeValue, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingAttribute")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -206,7 +216,7 @@ public CompletionStage validateThingScopedDeletion(@Nullable final ThingDe reduceSubmodelMapKeyToFeatureId(subModels), resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingScopedDeletion")) ) )) @@ -226,7 +236,7 @@ public CompletionStage validateThingActionInput(@Nullable final ThingDefin selectValidation(validationConfig) .validateThingActionInput(thingModel, messageSubject, inputPayload, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingActionInput")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -244,7 +254,7 @@ public CompletionStage validateThingActionOutput(@Nullable final ThingDefi selectValidation(validationConfig) .validateThingActionOutput(thingModel, messageSubject, outputPayload, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingActionOutput")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -262,7 +272,7 @@ public CompletionStage validateThingEventData(@Nullable final ThingDefinit selectValidation(validationConfig) .validateThingEventData(thingModel, messageSubject, dataPayload, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateThingEventData")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -277,6 +287,7 @@ public CompletionStage validateFeatures(@Nullable final ThingDefinition th return provideValidationConfigIfWotValidationEnabled(context) .map(validationConfig -> fetchResolveAndValidateWith(thingDefinition, dittoHeaders, thingModel -> doValidateFeatures(thingModel, features, resourcePath, context, validationConfig) + .handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatures")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -292,6 +303,7 @@ public CompletionStage validateFeatures(final ThingDefinition thingDefinit return provideValidationConfigIfWotValidationEnabled(context) .map(validationConfig -> doValidateFeatures(thingModel, features, resourcePath, context, validationConfig) + .handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatures")) ) .orElseGet(DefaultWotThingModelValidator::success); } @@ -310,13 +322,13 @@ public CompletionStage validateFeature(@Nullable final ThingDefinition thi return urlOpt.map(url -> fetchResolveAndValidateWith(url, dittoHeaders, thingModel -> doValidateFeature(thingModel, featureDefinition, feature, resourcePath, context, validationConfig - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeature")) ) ) .orElseGet(() -> doValidateFeature(null, featureDefinition, feature, resourcePath, context, validationConfig - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeature")) ); }) .orElseGet(DefaultWotThingModelValidator::success); @@ -336,7 +348,7 @@ public CompletionStage validateFeature(@Nullable final ThingDefinition thi .map(validationConfig -> doValidateFeature(thingModel, featureDefinition, featureThingModel, feature, resourcePath, context, validationConfig - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeature")) ) .orElseGet(DefaultWotThingModelValidator::success); } @@ -358,6 +370,7 @@ public CompletionStage validateFeatureDefinitionModification(@Nullable fin featureThingModel -> selectValidation(validationConfig) .validateFeature(featureThingModel, feature, resourcePath, context) + .handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureDefinitionModification")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -391,7 +404,7 @@ public CompletionStage validateFeatureProperties(@Nullable final ThingDefi Optional.ofNullable(featureDefinition).orElseThrow(), featureThingModelWithExtensionsAndImports, featureId, featureProperties, desiredProperties, resourcePath, dittoHeaders - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureProperties")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -412,7 +425,8 @@ public CompletionStage validateFeatureProperties(@Nullable final ThingDefi selectValidation(validationConfig) .validateFeatureProperties(featureThingModel, featureId, featureProperties, desiredProperties, resourcePath, context - )) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureProperties")) + ) .orElseGet(DefaultWotThingModelValidator::success); } @@ -435,7 +449,7 @@ public CompletionStage validateFeatureProperty(@Nullable final ThingDefini .validateFeatureProperty(featureThingModel, featureId, propertyPointer, propertyValue, desiredProperty, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureProperty")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -462,7 +476,7 @@ public CompletionStage validateFeatureScopedDeletion(@Nullable final Thing reduceSubmodelMapKeyToFeatureId(subModels), featureThingModel, featureId, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureScopedDeletion")) ) ) )) @@ -486,7 +500,7 @@ public CompletionStage validateFeatureActionInput(@Nullable final ThingDef selectValidation(validationConfig) .validateFeatureActionInput(featureThingModel, featureId, messageSubject, inputPayload, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureActionInput")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -508,7 +522,7 @@ public CompletionStage validateFeatureActionOutput(@Nullable final ThingDe selectValidation(validationConfig) .validateFeatureActionOutput(featureThingModel, featureId, messageSubject, inputPayload, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureActionOutput")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -530,7 +544,7 @@ public CompletionStage validateFeatureEventData(@Nullable final ThingDefin selectValidation(validationConfig) .validateFeatureEventData(featureThingModel, featureId, messageSubject, dataPayload, resourcePath, context - ) + ).handle(applyLogingErrorOnlyStrategy(validationConfig, context, "validateFeatureEventData")) )) .orElseGet(DefaultWotThingModelValidator::success); } @@ -554,6 +568,35 @@ private static CompletionStage success() { return CompletableFuture.completedStage(null); } + private static BiFunction applyLogingErrorOnlyStrategy( + final TmValidationConfig validationConfig, + final ValidationContext context, + final String loggingHintSource + ) { + return (aVoid, throwable) -> { + if (throwable != null) { + if (validationConfig.logWarningInsteadOfFailingApiCalls()) { + context.dittoHeaders().getCorrelationId().ifPresent(cId -> MDC.put("correlation-id", cId)); + log.warn("WoT based validation in <{}()> failed for / due to: <{}>", loggingHintSource, + context.thingDefinition(), context.featureDefinition(), throwable.getMessage(), throwable + ); + context.dittoHeaders().getCorrelationId().ifPresent(cId -> MDC.remove("correlation-id")); + return null; + } else { + final Throwable cause = + (throwable instanceof CompletionException ce) ? ce.getCause() : throwable; + if (cause instanceof RuntimeException re) { + throw re; + } else { + throw new IllegalStateException(cause); + } + } + } else { + return aVoid; + } + }; + } + private CompletionStage fetchResolveAndValidateWith(@Nullable final DefinitionIdentifier definitionIdentifier, final DittoHeaders dittoHeaders, final Function> validationFunction diff --git a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/TmValidationConfig.java b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/TmValidationConfig.java index a774a5cc6d..3697b24af2 100644 --- a/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/TmValidationConfig.java +++ b/wot/validation/src/main/java/org/eclipse/ditto/wot/validation/config/TmValidationConfig.java @@ -32,6 +32,11 @@ public interface TmValidationConfig { */ boolean isEnabled(); + /** + * @return whether to instead of to reject/fail API calls (when enabled=true), log a WARNING log instead + */ + boolean logWarningInsteadOfFailingApiCalls(); + /** * @return the config for validating things. */ @@ -60,7 +65,12 @@ enum ConfigValue implements KnownConfigValue { /** * Whether the TM based validation should be enabled or not. */ - ENABLED("enabled", true); + ENABLED("enabled", true), + + /** + * Whether to instead of to reject/fail API calls (when {@code enabled=true}), log a WARNING log instead. + */ + LOG_WARNING_INSTEAD_OF_FAILING_API_CALLS("log-warning-instead-of-failing-api-calls", false); private final String path;