diff --git a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/Service.java b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/Service.java index d03d02914..48dd1d65d 100644 --- a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/Service.java +++ b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/Service.java @@ -35,6 +35,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.api.paging.PagingInfo; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.PersistenceException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotFoundException; +import de.fraunhofer.iosb.ilt.faaast.service.model.messagebus.SubscriptionId; import de.fraunhofer.iosb.ilt.faaast.service.model.serialization.DataFormat; import de.fraunhofer.iosb.ilt.faaast.service.persistence.AssetAdministrationShellSearchCriteria; import de.fraunhofer.iosb.ilt.faaast.service.persistence.ConceptDescriptionSearchCriteria; @@ -73,6 +74,8 @@ public class Service implements ServiceContext { private static final Logger LOGGER = LoggerFactory.getLogger(Service.class); + private static final String VALUE_NULL = "value must not be null"; + private static final String ELEMENT_NULL = "element must not be null"; private final ServiceConfig config; private AssetConnectionManager assetConnectionManager; private List endpoints; @@ -84,6 +87,8 @@ public class Service implements ServiceContext { private RegistrySynchronization registrySynchronization; private RequestHandlerManager requestHandler; + private List submodelTemplateProcessors; + private List subscriptions; /** * Creates a new instance of {@link Service}. @@ -121,6 +126,8 @@ public Service(CoreConfig coreConfig, else { this.endpoints = endpoints; } + this.submodelTemplateProcessors = submodelTemplateProcessors; + this.subscriptions = new ArrayList<>(); this.config = ServiceConfig.builder() .core(coreConfig) .build(); @@ -149,6 +156,7 @@ public Service(ServiceConfig config) throws ConfigurationException, AssetConnectionException, PersistenceException, MessageBusException { Ensure.requireNonNull(config, "config must be non-null"); this.config = config; + this.subscriptions = new ArrayList<>(); init(); } @@ -306,7 +314,7 @@ public void stop() { } - private void init() throws ConfigurationException { + private void init() throws ConfigurationException, PersistenceException, MessageBusException { Ensure.requireNonNull(config.getPersistence(), new InvalidConfigurationException("config.persistence must be non-null")); Ensure.requireNonNull(config.getFileStorage(), new InvalidConfigurationException("config.filestorage must be non-null")); Ensure.requireNonNull(config.getMessageBus(), new InvalidConfigurationException("config.messagebus must be non-null")); diff --git a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/dataformat/environment/deserializer/AasxEnvironmentDeserializer.java b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/dataformat/environment/deserializer/AasxEnvironmentDeserializer.java index c0fc97a5f..feb03b255 100644 --- a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/dataformat/environment/deserializer/AasxEnvironmentDeserializer.java +++ b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/dataformat/environment/deserializer/AasxEnvironmentDeserializer.java @@ -23,6 +23,7 @@ import java.io.InputStream; import java.nio.charset.Charset; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.openxml4j.util.ZipSecureFile; import org.eclipse.digitaltwin.aas4j.v3.dataformat.aasx.AASXDeserializer; @@ -35,6 +36,8 @@ public class AasxEnvironmentDeserializer implements EnvironmentDeserializer { @Override public EnvironmentContext read(InputStream in, Charset charset) throws DeserializationException { try { + // temporary workaround to make sure, that all AASX files can be loaded. + ZipSecureFile.setMinInflateRatio(0); AASXDeserializer deserializer = new AASXDeserializer(in); return EnvironmentContext.builder() .environment(deserializer.read()) diff --git a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/Persistence.java b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/Persistence.java index a1ac6dbf2..5b8f8bd25 100644 --- a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/Persistence.java +++ b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/Persistence.java @@ -24,6 +24,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceAlreadyExistsException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotAContainerElementException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotFoundException; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; import de.fraunhofer.iosb.ilt.faaast.service.util.Ensure; import de.fraunhofer.iosb.ilt.faaast.service.util.ReferenceHelper; import java.util.Objects; @@ -233,6 +234,21 @@ public Page findAssetAdministrationShells(AssetAdminis throws PersistenceException; + /** + * Finds {@code org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell}s by search criteria and query. + * + * @param criteria the search criteria + * @param modifier the modifier + * @param paging paging information + * @param query the query to be executed + * @return the found {@code org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell}s + * @throws PersistenceException if there was an error with the storage. + */ + public Page findAssetAdministrationShellsWithQuery(AssetAdministrationShellSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, + Query query) + throws PersistenceException; + + /** * Finds {@code org.eclipse.digitaltwin.aas4j.v3.model.Submodel}s by search criteria. * @@ -245,6 +261,19 @@ public Page findAssetAdministrationShells(AssetAdminis public Page findSubmodels(SubmodelSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) throws PersistenceException; + /** + * Finds {@code org.eclipse.digitaltwin.aas4j.v3.model.Submodel}s by search criteria and query. + * + * @param criteria the search criteria + * @param modifier the modifier + * @param paging paging information + * @param query query to execute + * @return the found {@code org.eclipse.digitaltwin.aas4j.v3.model.Submodel}s + * @throws PersistenceException if there was an error with the storage. + */ + public Page findSubmodelsWithQuery(SubmodelSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) throws PersistenceException; + + /** * Finds {@code org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement}s by search criteria. * @@ -271,6 +300,20 @@ public Page findSubmodelElements(SubmodelElementSearchCriteria public Page findConceptDescriptions(ConceptDescriptionSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) throws PersistenceException; + /** + * Finds {@code org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription}s by search criteria and query. + * + * @param criteria the search criteria + * @param modifier the modifier + * @param paging paging information + * @param query query to execute + * @return the found {@code org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription}s + * @throws PersistenceException if there was an error with the storage. + */ + public Page findConceptDescriptionsWithQuery(ConceptDescriptionSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) + throws PersistenceException; + + /** * Save an {@code org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell}. * diff --git a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/query/QueryEvaluator.java b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/query/QueryEvaluator.java new file mode 100644 index 000000000..fc11b9d7f --- /dev/null +++ b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/query/QueryEvaluator.java @@ -0,0 +1,933 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.query; + +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.LogicalExpression; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.MatchExpression; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.StringValue; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Value; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription; +import org.eclipse.digitaltwin.aas4j.v3.model.Identifiable; +import org.eclipse.digitaltwin.aas4j.v3.model.Key; +import org.eclipse.digitaltwin.aas4j.v3.model.LangStringTextType; +import org.eclipse.digitaltwin.aas4j.v3.model.MultiLanguageProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.Reference; +import org.eclipse.digitaltwin.aas4j.v3.model.SpecificAssetId; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Evaluates queries sent to /query endpoints. + */ +public class QueryEvaluator { + + private static final Logger LOGGER = LoggerFactory.getLogger(QueryEvaluator.class); + private static final String PREFIX_AAS = "$aas#"; + private static final String PREFIX_SM = "$sm#"; + private static final String PREFIX_SME = "$sme"; + private static final String PREFIX_CD = "$cd#"; + + public QueryEvaluator() {} + + private enum ComparisonOperator { + EQ, + NE, + GT, + GE, + LT, + LE, + CONTAINS, + STARTS_WITH, + ENDS_WITH, + REGEX; + + boolean isStringOperator() { + return this == CONTAINS || this == STARTS_WITH || this == ENDS_WITH || this == REGEX; + } + } + + private enum ValueKind { + NONE, + FIELD, + STR, + NUM, + HEX, + DATETIME, + TIME, + BOOL, + STR_CAST, + NUM_CAST + } + + private enum StringValueKind { + NONE, + FIELD, + STR, + STR_CAST + } + + /** + * Used to decide whether to filter out the Identifiable. + * + * @param expr logical expression (tree) + * @param identifiable AAS | Submodel | ConceptDescription + * @return true if expression matches the identifiable + * + */ + public boolean matches(LogicalExpression expr, Identifiable identifiable) { + if (expr == null || identifiable == null) { + return false; + } + + // boolean + if (expr.get$boolean() != null) { + return expr.get$boolean(); + } + + // logical + if (expr.get$and() != null && !expr.get$and().isEmpty()) { + return expr.get$and().stream().allMatch(e -> matches(e, identifiable)); + } + if (expr.get$or() != null && !expr.get$or().isEmpty()) { + return expr.get$or().stream().anyMatch(e -> matches(e, identifiable)); + } + if (expr.get$not() != null) { + return !matches(expr.get$not(), identifiable); + } + + // match operator + if (expr.get$match() != null && !expr.get$match().isEmpty()) { + return evaluateMatch(expr.get$match(), identifiable); + } + + // numeric/boolean/string comparisons + boolean evaluated = evaluateFirstValueOperator(expr, identifiable); + if (evaluated) { + return true; + } + + // string binary operators + return evaluateFirstStringOperator(expr, identifiable); + } + + + private boolean evaluateFirstValueOperator(LogicalExpression expr, Identifiable identifiable) { + List> operations = Arrays.asList( + new OperationSpec<>(ComparisonOperator.EQ, expr::get$eq), + new OperationSpec<>(ComparisonOperator.NE, expr::get$ne), + new OperationSpec<>(ComparisonOperator.GT, expr::get$gt), + new OperationSpec<>(ComparisonOperator.GE, expr::get$ge), + new OperationSpec<>(ComparisonOperator.LT, expr::get$lt), + new OperationSpec<>(ComparisonOperator.LE, expr::get$le)); + for (OperationSpec spec: operations) { + List args = spec.argumentProvider.get(); + if (args != null && !args.isEmpty()) { + return evaluateBinaryComparison(args, identifiable, spec.operator); + } + } + return false; + } + + + private boolean evaluateFirstStringOperator(LogicalExpression expr, Identifiable identifiable) { + List> operations = Arrays.asList( + new OperationSpec<>(ComparisonOperator.CONTAINS, expr::get$contains), + new OperationSpec<>(ComparisonOperator.STARTS_WITH, expr::get$startsWith), + new OperationSpec<>(ComparisonOperator.ENDS_WITH, expr::get$endsWith), + new OperationSpec<>(ComparisonOperator.REGEX, expr::get$regex)); + for (OperationSpec spec: operations) { + List args = spec.argumentProvider.get(); + if (args != null && !args.isEmpty()) { + return evaluateBinaryStringOperator(args, identifiable, spec.operator); + } + } + return false; + } + + /** + * @param argumentProvider provides arguments for this operator + */ + private record OperationSpec(ComparisonOperator operator, Supplier> argumentProvider) {} + + private boolean evaluateBinaryComparison(List args, Identifiable identifiable, ComparisonOperator operator) { + if (args.size() < 2) { + LOGGER.error("Operator {} requires two arguments", operator); + return false; + } + List left = evaluateValue(args.get(0), identifiable); + List right = evaluateValue(args.get(1), identifiable); + return anyPairSatisfies(left, right, operator); + } + + + private boolean evaluateBinaryStringOperator(List args, Identifiable identifiable, ComparisonOperator operator) { + if (args.size() < 2) { + LOGGER.error("String operator {} requires two arguments", operator); + return false; + } + List left = evaluateStringValue(args.get(0), identifiable); + List right = evaluateStringValue(args.get(1), identifiable); + return anyPairSatisfies(left, right, operator); + } + + + private boolean anyPairSatisfies(List left, List right, ComparisonOperator operator) { + if (left == null || right == null) { + return false; + } + for (Object l: left) { + for (Object r: right) { + if (compareValues(l, r, operator)) { + return true; + } + } + } + return false; + } + + + private ValueKind determineValueKind(Value v) { + if (v == null) + return ValueKind.NONE; + if (v.get$field() != null) + return ValueKind.FIELD; + if (v.get$strVal() != null) + return ValueKind.STR; + if (v.get$numVal() != null) + return ValueKind.NUM; + if (v.get$hexVal() != null) + return ValueKind.HEX; + if (v.get$dateTimeVal() != null) + return ValueKind.DATETIME; + if (v.get$timeVal() != null) + return ValueKind.TIME; + if (v.get$boolean() != null) + return ValueKind.BOOL; + if (v.get$strCast() != null) + return ValueKind.STR_CAST; + if (v.get$numCast() != null) + return ValueKind.NUM_CAST; + return ValueKind.NONE; + } + + + private StringValueKind determineStringValueKind(StringValue sv) { + if (sv == null) + return StringValueKind.NONE; + if (sv.get$field() != null) + return StringValueKind.FIELD; + if (sv.get$strVal() != null) + return StringValueKind.STR; + if (sv.get$strCast() != null) + return StringValueKind.STR_CAST; + return StringValueKind.NONE; + } + + + private List evaluateValue(Value v, Identifiable identifiable) { + return switch (determineValueKind(v)) { + case FIELD -> nonNull(getFieldValues(v.get$field(), identifiable)); + case STR -> Collections.singletonList(v.get$strVal()); + case NUM -> Collections.singletonList(v.get$numVal()); + case HEX -> Collections.singletonList(v.get$hexVal()); + case DATETIME -> Collections.singletonList(v.get$dateTimeVal()); + case TIME -> Collections.singletonList(v.get$timeVal()); + case BOOL -> Collections.singletonList(v.get$boolean()); + case STR_CAST -> evaluateValue(v.get$strCast(), identifiable).stream() + .map(String::valueOf).collect(Collectors.toList()); + case NUM_CAST -> evaluateValue(v.get$numCast(), identifiable).stream() + .map(String::valueOf) + .map(this::parseDoubleOrNull) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + default -> Collections.emptyList(); + }; + } + + + private List evaluateStringValue(StringValue sv, Identifiable identifiable) { + return switch (determineStringValueKind(sv)) { + case FIELD -> nonNull(getFieldValues(sv.get$field(), identifiable)); + case STR -> Collections.singletonList(sv.get$strVal()); + case STR_CAST -> evaluateValue(sv.get$strCast(), identifiable).stream() + .map(String::valueOf) + .collect(Collectors.toList()); + default -> { + LOGGER.error("Invalid string value: {}", sv); + yield Collections.emptyList(); + } + }; + } + + + private Double parseDoubleOrNull(String s) { + try { + return Double.valueOf(s); + } + catch (NumberFormatException e) { + return null; + } + } + + + private Double toDouble(Object o) { + if (o instanceof Number) { + return ((Number) o).doubleValue(); + } + return parseDoubleOrNull(String.valueOf(o)); + } + + /** + * @param suffix e.g., ".name", "Sub.Path#value" + */ + private record Condition(String suffix, ComparisonOperator operator, List rightVals) { + private Condition(String suffix, ComparisonOperator operator, List rightVals) { + this.suffix = suffix; + this.operator = operator; + this.rightVals = rightVals != null ? rightVals : Collections.emptyList(); + } + } + + private record MatchOperation(ComparisonOperator operator, List args) {} + + private record MatchEvaluationContext(String commonPrefix, List itemConditions, boolean directMismatch) {} + + private boolean evaluateMatch(List matches, Identifiable identifiable) { + if (matches == null || matches.isEmpty()) { + return true; + } + MatchEvaluationContext ctx = buildMatchEvaluationContext(matches, identifiable); + if (ctx.directMismatch) { + return false; + } + if (ctx.commonPrefix == null) { + return true; + } + return evaluateListMatch(ctx.commonPrefix, ctx.itemConditions, identifiable); + } + + + private MatchEvaluationContext buildMatchEvaluationContext(List matches, Identifiable identifiable) { + String commonPrefix = null; + List itemConditions = new ArrayList<>(); + boolean directMismatch = false; + + for (MatchExpression m: matches) { + MatchOperation mo = getMatchOperation(m); + if (mo == null) { + LOGGER.error("Unsupported operator in match"); + directMismatch = true; + break; + } + if (mo.args.size() < 2) { + LOGGER.error("$match operator {} requires two arguments", mo.operator); + directMismatch = true; + break; + } + + Value left = mo.args.get(0); + Value right = mo.args.get(1); + if (left.get$field() == null) { + LOGGER.error("Left side in $match must be a field: {}", left); + directMismatch = true; + break; + } + + String field = left.get$field(); + List rightVals = evaluateValue(right, identifiable); + + int listMarker = field.indexOf("[]"); + if (listMarker == -1) { + if (field.startsWith(PREFIX_SME + "#")) { + String prefix = PREFIX_SME; + if (commonPrefix != null && !commonPrefix.equals(prefix)) { + LOGGER.error("Non-common prefix in match: {} vs {}", commonPrefix, prefix); + directMismatch = true; + break; + } + commonPrefix = prefix; + String suffix = field.substring((PREFIX_SME + "#").length()); + itemConditions.add(new Condition(suffix, mo.operator, rightVals)); + } + else { + // evaluate parent condition immediately + List leftVals = evaluateValue(left, identifiable); + if (!anyPairSatisfies(leftVals, rightVals, mo.operator)) { + directMismatch = true; + break; + } + } + } + else { + String prefix = field.substring(0, listMarker); + if (commonPrefix != null && !commonPrefix.equals(prefix)) { + LOGGER.error("Non-common prefix in match: {} vs {}", commonPrefix, prefix); + directMismatch = true; + break; + } + commonPrefix = prefix; + String suffix = field.substring(listMarker + 2); + itemConditions.add(new Condition(suffix, mo.operator, rightVals)); + } + } + + return new MatchEvaluationContext(commonPrefix, itemConditions, directMismatch); + } + + + private boolean evaluateListMatch(String commonPrefix, List itemConditions, Identifiable identifiable) { + switch (commonPrefix) { + case "$aas#assetInformation.specificAssetIds": + if (!(identifiable instanceof AssetAdministrationShell aas)) + return false; + if (aas.getAssetInformation() == null || aas.getAssetInformation().getSpecificAssetIds() == null) + return false; + + for (SpecificAssetId item: aas.getAssetInformation().getSpecificAssetIds()) { + if (doAllItemConditionsMatch(itemConditions, cond -> { + String s = getSpecificAssetIdAttribute(item, cond.suffix); + return s == null ? Collections.emptyList() : Collections.singletonList(s); + })) { + return true; + } + } + return false; + + case PREFIX_SME: + if (!(identifiable instanceof Submodel sm)) + return false; + List topLevel = sm.getSubmodelElements(); + if (topLevel == null) + return false; + + for (SubmodelElement item: topLevel) { + if (doAllItemConditionsMatch(itemConditions, cond -> getPropertyValuesFromSuffix(item, cond.suffix))) { + return true; + } + } + return false; + + default: + if (commonPrefix.startsWith(PREFIX_SME + ".")) { + if (!(identifiable instanceof Submodel sm2)) + return false; + String path = commonPrefix.substring((PREFIX_SME + ".").length()); + SubmodelElement listElem = getSubmodelElementByPath(sm2, path); + if (!(listElem instanceof SubmodelElementList)) + return false; + + List items = ((SubmodelElementList) listElem).getValue(); + if (items == null) + return false; + + for (SubmodelElement item: items) { + if (doAllItemConditionsMatch(itemConditions, cond -> getPropertyValuesFromSuffix(item, cond.suffix))) { + return true; + } + } + return false; + } + LOGGER.error("Unsupported prefix for $match: {}", commonPrefix); + return false; + } + } + + + private MatchOperation getMatchOperation(MatchExpression m) { + List candidates = Arrays.asList( + new MatchOperation(ComparisonOperator.EQ, m.get$eq()), + new MatchOperation(ComparisonOperator.NE, m.get$ne()), + new MatchOperation(ComparisonOperator.GT, m.get$gt()), + new MatchOperation(ComparisonOperator.GE, m.get$ge()), + new MatchOperation(ComparisonOperator.LT, m.get$lt()), + new MatchOperation(ComparisonOperator.LE, m.get$le())); + for (MatchOperation mo: candidates) { + if (mo.args != null && !mo.args.isEmpty()) { + return mo; + } + } + return null; + } + + + private boolean doAllItemConditionsMatch(List conditions, + java.util.function.Function> leftValueExtractor) { + for (Condition cond: conditions) { + List leftVals = nonNull(leftValueExtractor.apply(cond)); + if (!anyPairSatisfies(leftVals, cond.rightVals, cond.operator)) { + return false; + } + } + return true; + } + + + private List getFieldValues(String field, Identifiable identifiable) { + if (field == null || identifiable == null) + return Collections.emptyList(); + + if (field.startsWith(PREFIX_AAS)) { + if (!(identifiable instanceof AssetAdministrationShell)) + return Collections.emptyList(); + return getAasFieldValues((AssetAdministrationShell) identifiable, field.substring(PREFIX_AAS.length())); + } + + if (field.startsWith(PREFIX_SM)) { + if (!(identifiable instanceof Submodel)) + return Collections.emptyList(); + return new ArrayList<>(getSubmodelAttributeValues((Submodel) identifiable, field.substring(PREFIX_SM.length()))); + } + + if (field.startsWith(PREFIX_SME)) { + if (!(identifiable instanceof Submodel sm)) + return Collections.emptyList(); + + String pathPart = ""; + String attr; + if (field.contains(".")) { + int hashPos = field.indexOf("#", field.indexOf(".")); + pathPart = field.substring(field.indexOf(".") + 1, hashPos); + attr = field.substring(hashPos + 1); + } + else { + attr = field.substring((PREFIX_SME + "#").length()); + } + + List values = new ArrayList<>(); + if (!pathPart.isEmpty()) { + SubmodelElement sme = getSubmodelElementByPath(sm, pathPart); + if (sme != null) { + values.addAll(getSubmodelElementAttributeValues(sme, attr)); + } + } + else { + List smes = sm.getSubmodelElements(); + if (smes != null) { + for (SubmodelElement sme: smes) { + values.addAll(getSubmodelElementAttributeValues(sme, attr)); + } + } + } + return values; + } + + if (field.startsWith(PREFIX_CD)) { + if (!(identifiable instanceof ConceptDescription cd)) + return Collections.emptyList(); + String attr = field.substring(PREFIX_CD.length()); + return switch (attr) { + case "idShort" -> Collections.singletonList(cd.getIdShort()); + case "id" -> Collections.singletonList(cd.getId()); + default -> { + LOGGER.error("Unsupported CD attribute: {}", attr); + yield Collections.emptyList(); + } + }; + } + + LOGGER.error("Unsupported field: {}", field); + return Collections.emptyList(); + } + + + private List getAasFieldValues(AssetAdministrationShell aas, String attr) { + switch (attr) { + case "idShort": + return Collections.singletonList(aas.getIdShort()); + case "id": + return Collections.singletonList(aas.getId()); + case "assetInformation.assetKind": + return (aas.getAssetInformation() == null || aas.getAssetInformation().getAssetKind() == null) + ? Collections.emptyList() + : Collections.singletonList(aas.getAssetInformation().getAssetKind().name()); + case "assetInformation.assetType": + return (aas.getAssetInformation() == null) + ? Collections.emptyList() + : Collections.singletonList(aas.getAssetInformation().getAssetType()); + case "assetInformation.globalAssetId": + if (aas.getAssetInformation() == null) + return Collections.emptyList(); + String globalAssetId = aas.getAssetInformation().getGlobalAssetId(); + return globalAssetId == null ? Collections.emptyList() : Collections.singletonList(globalAssetId); + default: + if (attr.startsWith("assetInformation.specificAssetIds")) { + if (aas.getAssetInformation() == null || aas.getAssetInformation().getSpecificAssetIds() == null) { + return Collections.emptyList(); + } + String remaining = attr.substring("assetInformation.specificAssetIds".length()); + IndexSelection indexSelection = parseIndexSelection(remaining); + List sais = aas.getAssetInformation().getSpecificAssetIds(); + List selectedItems = selectByIndex(sais, indexSelection); + List values = new ArrayList<>(); + for (SpecificAssetId sai: selectedItems) { + values.add(getSpecificAssetIdAttribute(sai, indexSelection.remainingSuffix)); + } + return values; + } + LOGGER.error("Unsupported AAS attribute: {}", attr); + return Collections.emptyList(); + } + } + + + private List getSubmodelAttributeValues(Submodel sm, String attr) { + switch (attr) { + case "idShort": + return Collections.singletonList(sm.getIdShort()); + case "id": + return Collections.singletonList(sm.getId()); + case "semanticId": { + Reference ref = sm.getSemanticId(); + if (ref == null || ref.getKeys() == null || ref.getKeys().isEmpty()) + return Collections.emptyList(); + return Collections.singletonList(ref.getKeys().get(0).getValue()); + } + default: + if (attr.startsWith("semanticId.keys")) { + Reference ref = sm.getSemanticId(); + if (ref == null || ref.getKeys() == null) + return Collections.emptyList(); + + String remaining = attr.substring("semanticId.keys".length()); + IndexSelection indexSelection = parseIndexSelection(remaining); + + List selectedItems = selectByIndex(ref.getKeys(), indexSelection); + return extractKeyAttributeValues(selectedItems, indexSelection); + } + LOGGER.error("Unsupported SM attribute: {}", attr); + return Collections.emptyList(); + } + } + + + private List getSubmodelElementAttributeValues(SubmodelElement sme, String attr) { + if (sme == null || attr == null) + return Collections.emptyList(); + + switch (attr) { + case "idShort": + return Collections.singletonList(sme.getIdShort()); + case "value": + if (sme instanceof Property) { + return Collections.singletonList(((Property) sme).getValue()); + } + return Collections.emptyList(); + case "valueType": + if (sme instanceof Property && ((Property) sme).getValueType() != null) { + return Collections.singletonList(((Property) sme).getValueType().name()); + } + return Collections.emptyList(); + case "language": + if (sme instanceof MultiLanguageProperty) { + List values = ((MultiLanguageProperty) sme).getValue(); + if (values == null) + return Collections.emptyList(); + return values.stream() + .filter(Objects::nonNull) + .map(LangStringTextType::getLanguage) + .collect(Collectors.toList()); + } + return Collections.emptyList(); + case "semanticId": { + Reference ref = sme.getSemanticId(); + if (ref == null || ref.getKeys() == null || ref.getKeys().isEmpty()) + return Collections.emptyList(); + return Collections.singletonList(ref.getKeys().get(0).getValue()); + } + default: + if (attr.startsWith("semanticId.keys")) { + Reference ref = sme.getSemanticId(); + if (ref == null || ref.getKeys() == null) + return Collections.emptyList(); + + String remaining = attr.substring("semanticId.keys".length()); + IndexSelection indexSelection = parseIndexSelection(remaining); + + List selectedItems = selectByIndex(ref.getKeys(), indexSelection); + return extractKeyAttributeValues(selectedItems, indexSelection); + } + LOGGER.error("Unsupported SME attribute: {}", attr); + return Collections.emptyList(); + } + } + + + private static List extractKeyAttributeValues(List selectedItems, IndexSelection selector) { + List results = new ArrayList<>(); + for (Key key: selectedItems) { + switch (selector.remainingSuffix) { + case ".type": + results.add(key.getType().name()); + break; + case ".value": + results.add(key.getValue()); + break; + default: + break; + } + } + return results; + } + + + /** + * Resolve a submodel element by dot-separated path. + */ + private SubmodelElement getSubmodelElementByPath(Submodel sm, String path) { + if (sm == null || path == null || path.isEmpty()) + return null; + + String[] tokens = path.split("\\."); + SubmodelElement current = null; + + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i]; + if (i == 0) { + current = findByIdShort(sm.getSubmodelElements(), token); + } + else { + if (current instanceof SubmodelElementCollection) { + current = findByIdShort(((SubmodelElementCollection) current).getValue(), token); + } + else if (current instanceof SubmodelElementList) { + current = findByIdShort(((SubmodelElementList) current).getValue(), token); + } + else { + return null; + } + } + if (current == null) + return null; + } + return current; + } + + + private SubmodelElement findByIdShort(List elements, String idShort) { + if (elements == null || idShort == null) + return null; + return elements.stream().filter(e -> idShort.equals(e.getIdShort())).findFirst().orElse(null); + } + + + private String getSpecificAssetIdAttribute(Object item, String path) { + if (!(item instanceof SpecificAssetId sai) || path == null) { + LOGGER.error("Unsupported property {} for object {}", path, item); + return null; + } + switch (path) { + case ".name": + return sai.getName(); + case ".value": + return sai.getValue(); + default: + if (path.startsWith(".externalSubjectId") && sai.getExternalSubjectId() != null) { + return String.valueOf(sai.getExternalSubjectId()); + } + } + LOGGER.error("Unsupported property: {}", path); + return null; + } + + + private List getPropertyValuesFromSuffix(SubmodelElement item, String suffix) { + if (item == null || suffix == null) + return Collections.emptyList(); + + String normalized = suffix.startsWith(".") ? suffix.substring(1) : suffix; + String subPath = ""; + String attr = normalized; + int hashPos = normalized.indexOf('#'); + if (hashPos != -1) { + subPath = normalized.substring(0, hashPos); + attr = normalized.substring(hashPos + 1); + } + SubmodelElement target = resolveRelativeSubmodelElementPath(item, subPath); + if (target == null) { + return Collections.emptyList(); + } + return new ArrayList<>(getSubmodelElementAttributeValues(target, attr)); + } + + + private SubmodelElement resolveRelativeSubmodelElementPath(SubmodelElement item, String path) { + if (item == null || path == null || path.isEmpty()) + return item; + + List tokens = Arrays.asList(path.split("\\.")); + SubmodelElement current = item; + if (!tokens.isEmpty() && tokens.get(0).equals(current.getIdShort())) { + tokens = tokens.subList(1, tokens.size()); + } + for (String token: tokens) { + if (!(current instanceof SubmodelElementCollection)) { + return null; + } + current = findByIdShort(((SubmodelElementCollection) current).getValue(), token); + if (current == null) + return null; + } + return current; + } + + + private boolean compareValues(Object a, Object b, ComparisonOperator operator) { + if (operator == null) + return false; + + if (operator.isStringOperator()) { + return compareUsingStringOperator(a, b, operator); + } + return compareUsingGeneralComparison(a, b, operator); + } + + + private boolean compareUsingStringOperator(Object a, Object b, ComparisonOperator operator) { + if (a == null || b == null) + return false; + String left = String.valueOf(a); + String right = String.valueOf(b); + + return switch (operator) { + case CONTAINS -> left.contains(right); + case STARTS_WITH -> left.startsWith(right); + case ENDS_WITH -> left.endsWith(right); + case REGEX -> Pattern.compile(right).matcher(left).matches(); + default -> false; + }; + } + + + private boolean compareUsingGeneralComparison(Object a, Object b, ComparisonOperator operator) { + if (a == null || b == null) { + return (operator == ComparisonOperator.EQ) + ? Objects.equals(a, b) + : (operator == ComparisonOperator.NE) && !Objects.equals(a, b); + } + + // try numeric + Double d1 = toDouble(a); + Double d2 = toDouble(b); + if (d1 != null && d2 != null) { + return switch (operator) { + case EQ -> Double.compare(d1, d2) == 0; + case NE -> Double.compare(d1, d2) != 0; + case GT -> d1 > d2; + case GE -> d1 >= d2; + case LT -> d1 < d2; + case LE -> d1 <= d2; + default -> false; + }; + } + + // try boolean + String sa = String.valueOf(a).trim(); + String sb = String.valueOf(b).trim(); + Boolean ba = parseBooleanStrict(sa); + Boolean bb = parseBooleanStrict(sb); + if (ba != null && bb != null) { + return switch (operator) { + case EQ -> Objects.equals(ba, bb); + case NE -> !Objects.equals(ba, bb); + default -> false; + }; + } + + // string comparison + int cmp = sa.compareTo(sb); + return switch (operator) { + case EQ -> cmp == 0; + case NE -> cmp != 0; + case GT -> cmp > 0; + case GE -> cmp >= 0; + case LT -> cmp < 0; + case LE -> cmp <= 0; + default -> false; + }; + } + + + private static Boolean parseBooleanStrict(String s) { + if ("true".equalsIgnoreCase(s)) + return Boolean.TRUE; + if ("false".equalsIgnoreCase(s)) + return Boolean.FALSE; + return null; + } + + + private static List nonNull(List in) { + return in != null ? in : Collections.emptyList(); + } + + /** + * @param remainingSuffix remaining suffix (e.g., ".name") + */ + private record IndexSelection(boolean selectAll, Integer index, String remainingSuffix) {} + + private IndexSelection parseIndexSelection(String s) { + if (s == null || s.isEmpty()) { + return new IndexSelection(true, null, ""); + } + String rem = s; + boolean selectAll = false; + Integer idx = null; + + if (rem.startsWith("[]")) { + selectAll = true; + rem = rem.substring(2); + } + else if (rem.startsWith("[")) { + int end = rem.indexOf(']'); + if (end > 1) { + String idxStr = rem.substring(1, end); + try { + idx = Integer.parseInt(idxStr); + } + catch (NumberFormatException e) { + LOGGER.error("Invalid index in path: {}", s); + return new IndexSelection(true, null, rem.substring(end + 1)); + } + rem = rem.substring(end + 1); + } + } + return new IndexSelection(selectAll, idx, rem); + } + + + private List selectByIndex(List list, IndexSelection selector) { + if (list == null || list.isEmpty()) + return Collections.emptyList(); + if (selector.selectAll || selector.index == null) + return list; + int i = selector.index; + return (i >= 0 && i < list.size()) ? Collections.singletonList(list.get(i)) : Collections.emptyList(); + } +} diff --git a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/aasrepository/QueryAssetAdministrationShellsRequestHandler.java b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/aasrepository/QueryAssetAdministrationShellsRequestHandler.java new file mode 100644 index 000000000..a1ef57b49 --- /dev/null +++ b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/aasrepository/QueryAssetAdministrationShellsRequestHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.request.handler.aasrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.exception.MessageBusException; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.paging.Page; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository.QueryAssetAdministrationShellsRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.aasrepository.QueryAssetAdministrationShellsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.PersistenceException; +import de.fraunhofer.iosb.ilt.faaast.service.model.messagebus.event.access.ElementReadEventMessage; +import de.fraunhofer.iosb.ilt.faaast.service.persistence.AssetAdministrationShellSearchCriteria; +import de.fraunhofer.iosb.ilt.faaast.service.request.handler.AbstractRequestHandler; +import de.fraunhofer.iosb.ilt.faaast.service.request.handler.RequestExecutionContext; +import de.fraunhofer.iosb.ilt.faaast.service.util.LambdaExceptionHelper; +import java.util.Objects; +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; + + +/** + * Class to handle a + * {@link de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository.QueryAssetAdministrationShellsRequest} + * in the service and to send the corresponding response + * {@link de.fraunhofer.iosb.ilt.faaast.service.model.api.response.aasrepository.QueryAssetAdministrationShellsResponse}. + * Is responsible for communication with the persistence and sends the corresponding events to the message bus. + */ +public class QueryAssetAdministrationShellsRequestHandler extends AbstractRequestHandler { + + @Override + public QueryAssetAdministrationShellsResponse process(QueryAssetAdministrationShellsRequest request, RequestExecutionContext context) + throws MessageBusException, PersistenceException { + Page page = context.getPersistence().findAssetAdministrationShellsWithQuery( + AssetAdministrationShellSearchCriteria.NONE, + request.getOutputModifier(), + request.getPagingInfo(), + request.getQuery()); + if (!request.isInternal() && Objects.nonNull(page.getContent())) { + page.getContent().forEach(LambdaExceptionHelper.rethrowConsumer( + x -> context.getMessageBus().publish(ElementReadEventMessage.builder() + .element(x) + .value(x) + .build()))); + } + return QueryAssetAdministrationShellsResponse.builder() + .payload(page) + .success() + .build(); + } + +} diff --git a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/conceptdescription/QueryConceptDescriptionsRequestHandler.java b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/conceptdescription/QueryConceptDescriptionsRequestHandler.java new file mode 100644 index 000000000..9988deeda --- /dev/null +++ b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/conceptdescription/QueryConceptDescriptionsRequestHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.request.handler.conceptdescription; + +import de.fraunhofer.iosb.ilt.faaast.service.exception.MessageBusException; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.paging.Page; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.QueryConceptDescriptionsRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.conceptdescription.QueryConceptDescriptionsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.PersistenceException; +import de.fraunhofer.iosb.ilt.faaast.service.model.messagebus.event.access.ElementReadEventMessage; +import de.fraunhofer.iosb.ilt.faaast.service.persistence.ConceptDescriptionSearchCriteria; +import de.fraunhofer.iosb.ilt.faaast.service.request.handler.AbstractRequestHandler; +import de.fraunhofer.iosb.ilt.faaast.service.request.handler.RequestExecutionContext; +import de.fraunhofer.iosb.ilt.faaast.service.util.LambdaExceptionHelper; +import java.util.Objects; +import org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription; + + +/** + * Class to handle a + * {@link QueryConceptDescriptionsRequest} + * in the service and to send the corresponding response + * {@link QueryConceptDescriptionsResponse}. + * Is responsible for communication with the persistence and sends the corresponding events to the message bus. + */ +public class QueryConceptDescriptionsRequestHandler extends AbstractRequestHandler { + + @Override + public QueryConceptDescriptionsResponse process(QueryConceptDescriptionsRequest request, RequestExecutionContext context) + throws MessageBusException, PersistenceException { + Page page = context.getPersistence().findConceptDescriptionsWithQuery( + ConceptDescriptionSearchCriteria.NONE, + request.getOutputModifier(), + request.getPagingInfo(), + request.getQuery()); + if (!request.isInternal() && Objects.nonNull(page.getContent())) { + page.getContent().forEach(LambdaExceptionHelper.rethrowConsumer( + x -> context.getMessageBus().publish(ElementReadEventMessage.builder() + .element(x) + .value(x) + .build()))); + } + return QueryConceptDescriptionsResponse.builder() + .payload(page) + .success() + .build(); + } + +} diff --git a/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/submodelrepository/QuerySubmodelsRequestHandler.java b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/submodelrepository/QuerySubmodelsRequestHandler.java new file mode 100644 index 000000000..ea54a9c05 --- /dev/null +++ b/core/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/request/handler/submodelrepository/QuerySubmodelsRequestHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.request.handler.submodelrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.assetconnection.AssetConnectionException; +import de.fraunhofer.iosb.ilt.faaast.service.exception.MessageBusException; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.paging.Page; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.submodelrepository.QuerySubmodelsRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.submodelrepository.QuerySubmodelsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.PersistenceException; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotAContainerElementException; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotFoundException; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ValueMappingException; +import de.fraunhofer.iosb.ilt.faaast.service.model.messagebus.event.access.ElementReadEventMessage; +import de.fraunhofer.iosb.ilt.faaast.service.persistence.SubmodelSearchCriteria; +import de.fraunhofer.iosb.ilt.faaast.service.request.handler.AbstractRequestHandler; +import de.fraunhofer.iosb.ilt.faaast.service.request.handler.RequestExecutionContext; +import java.util.Objects; +import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.util.AasUtils; +import org.eclipse.digitaltwin.aas4j.v3.model.Reference; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; + + +/** + * Class to handle a + * {@link QuerySubmodelsRequest} + * in the service and to send the corresponding response + * {@link QuerySubmodelsResponse}. + * Is responsible for communication with the persistence and sends the corresponding events to the message bus. + */ +public class QuerySubmodelsRequestHandler extends AbstractRequestHandler { + + @Override + public QuerySubmodelsResponse process(QuerySubmodelsRequest request, RequestExecutionContext context) + throws MessageBusException, PersistenceException, ResourceNotAContainerElementException, ValueMappingException, ResourceNotFoundException, AssetConnectionException { + Page page = context.getPersistence().findSubmodelsWithQuery( + SubmodelSearchCriteria.NONE, + request.getOutputModifier(), + request.getPagingInfo(), + request.getQuery()); + if (Objects.nonNull(page.getContent())) { + for (Submodel submodel: page.getContent()) { + Reference reference = AasUtils.toReference(submodel); + syncWithAsset(reference, submodel.getSubmodelElements(), !request.isInternal(), context); + if (!request.isInternal()) { + context.getMessageBus().publish(ElementReadEventMessage.builder() + .element(reference) + .value(submodel) + .build()); + } + } + } + return QuerySubmodelsResponse.builder() + .payload(page) + .success() + .build(); + } + +} diff --git a/core/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/query/QueryEvaluatorTest.java b/core/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/query/QueryEvaluatorTest.java new file mode 100644 index 000000000..bc9a0a6bb --- /dev/null +++ b/core/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/query/QueryEvaluatorTest.java @@ -0,0 +1,560 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.query; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.AssetKind; +import org.eclipse.digitaltwin.aas4j.v3.model.DataTypeDefXsd; +import org.eclipse.digitaltwin.aas4j.v3.model.Environment; +import org.eclipse.digitaltwin.aas4j.v3.model.KeyTypes; +import org.eclipse.digitaltwin.aas4j.v3.model.Property; +import org.eclipse.digitaltwin.aas4j.v3.model.ReferenceTypes; +import org.eclipse.digitaltwin.aas4j.v3.model.SpecificAssetId; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElement; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.SubmodelElementList; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetAdministrationShell; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultAssetInformation; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultEnvironment; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultKey; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultLangStringTextType; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultMultiLanguageProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultReference; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSpecificAssetId; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodel; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementCollection; +import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultSubmodelElementList; +import org.junit.Test; + + +/** + * Unit tests for {@link QueryEvaluator}. + */ +public class QueryEvaluatorTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private Environment createTestEnvironmentForSimpleEq(boolean matching) { + Submodel submodel = new DefaultSubmodel.Builder() + .id("https://example.com/submodel/1") + .idShort("TestSubmodel") + .build(); + + AssetAdministrationShell aas = new DefaultAssetAdministrationShell.Builder() + .id("https://example.com/aas/1") + .idShort(matching ? "TestType" : "NonMatching") + .assetInformation(new DefaultAssetInformation.Builder() + .assetKind(AssetKind.INSTANCE) + .assetType("TestType") + .build()) + .build(); + + return new DefaultEnvironment.Builder() + .assetAdministrationShells(aas) + .submodels(submodel) + .build(); + } + + + private Environment createTestEnvironmentForDocumentsMatch(boolean matching) { + List documentsItems = new ArrayList<>(); + SubmodelElementCollection docItem = new DefaultSubmodelElementCollection.Builder() + .idShort("Doc1") + .value(new DefaultSubmodelElementCollection.Builder() + .idShort("DocumentClassification") + .value(new DefaultProperty.Builder() + .idShort("Class") + .value(matching ? "03-01" : "NonMatching") + .valueType(DataTypeDefXsd.STRING) + .build()) + .build()) + .value(new DefaultSubmodelElementCollection.Builder() + .idShort("DocumentVersion") + .value(new DefaultMultiLanguageProperty.Builder() + .idShort("SMLLanguages") + .value(new DefaultLangStringTextType.Builder() + .language("nl") + .text("Dutch text") + .build()) + .build()) + .build()) + .build(); + documentsItems.add(docItem); + + SubmodelElementList documents = new DefaultSubmodelElementList.Builder() + .idShort("Documents") + .value(documentsItems) + .build(); + + Submodel submodel = new DefaultSubmodel.Builder() + .id("https://example.com/submodel/2") + .idShort("TestSubmodel") + .submodelElements(documents) + .build(); + + AssetAdministrationShell aas = new DefaultAssetAdministrationShell.Builder() + .id("https://example.com/aas/2") + .idShort("TestAAS") + .assetInformation(new DefaultAssetInformation.Builder() + .assetKind(AssetKind.INSTANCE) + .build()) + .build(); + + return new DefaultEnvironment.Builder() + .assetAdministrationShells(aas) + .submodels(submodel) + .build(); + } + + + private Environment createTestEnvironmentForAndMatch(boolean matching) { + List productClassItems = new ArrayList<>(); + Property productClassId = new DefaultProperty.Builder() + .idShort("ProductClassId") + .value(matching ? "27-37-09-05" : "NonMatching") + .valueType(DataTypeDefXsd.STRING) + .build(); + productClassItems.add(productClassId); + + SubmodelElementList productClassifications = new DefaultSubmodelElementList.Builder() + .idShort("ProductClassifications") + .value(productClassItems) + .build(); + + Property someProperty = new DefaultProperty.Builder() + .idShort("SomeProperty") + .semanticId(new DefaultReference.Builder() + .type(ReferenceTypes.EXTERNAL_REFERENCE) + .keys(new DefaultKey.Builder() + .type(KeyTypes.GLOBAL_REFERENCE) + .value("0173-1#02-BAF016#006") + .build()) + .build()) + .value(matching ? "50" : "150") // For < 100 + .valueType(DataTypeDefXsd.INT) + .build(); + + Submodel submodel = new DefaultSubmodel.Builder() + .id("https://example.com/submodel/3") + .idShort("TechnicalData") + .submodelElements(productClassifications) + .submodelElements(someProperty) + .build(); + + AssetAdministrationShell aas = new DefaultAssetAdministrationShell.Builder() + .id("https://example.com/aas/3") + .idShort("TestAAS") + .assetInformation(new DefaultAssetInformation.Builder() + .assetKind(AssetKind.INSTANCE) + .build()) + .build(); + + return new DefaultEnvironment.Builder() + .assetAdministrationShells(aas) + .submodels(submodel) + .build(); + } + + + private Environment createTestEnvironmentForOrMatch(boolean matching) { + List specificAssetIds = new ArrayList<>(); + specificAssetIds.add(new DefaultSpecificAssetId.Builder() + .name("supplierId") + .value(matching ? "aas-1" : "NonMatching") + .build()); + specificAssetIds.add(new DefaultSpecificAssetId.Builder() + .name("customerId") + .value(matching ? "aas-2" : "NonMatching") + .build()); + + AssetAdministrationShell aas = new DefaultAssetAdministrationShell.Builder() + .id("https://example.com/aas/4") + .idShort("TestAAS") + .assetInformation(new DefaultAssetInformation.Builder() + .assetKind(AssetKind.INSTANCE) + .specificAssetIds(specificAssetIds) + .build()) + .build(); + + Submodel submodel = new DefaultSubmodel.Builder() + .id("https://example.com/submodel/4") + .idShort("TestSubmodel") + .build(); + + return new DefaultEnvironment.Builder() + .assetAdministrationShells(aas) + .submodels(submodel) + .build(); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void simpleEq_withMatchingFields() throws Exception { + String json = """ + { + "$condition": { + "$eq": [ + { "$field": "$aas#idShort" }, + { + "$field": + "$aas#assetInformation.assetType" + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForSimpleEq(true); + QueryEvaluator evaluator = new QueryEvaluator(); + AssetAdministrationShell aas = env.getAssetAdministrationShells().get(0); + boolean result = evaluator.matches(query.get$condition(), aas); + assertTrue(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void simpleEq_withNonMatchingFields() throws Exception { + String json = """ + { + "$condition": { + "$eq": [ + { "$field": "$aas#idShort" }, + { + "$field": + "$aas#assetInformation.assetType" + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForSimpleEq(false); + QueryEvaluator evaluator = new QueryEvaluator(); + AssetAdministrationShell aas = env.getAssetAdministrationShells().get(0); + boolean result = evaluator.matches(query.get$condition(), aas); + assertFalse(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void documentsMatch_withMatchingValues() throws Exception { + String json = """ + { + "$condition": { + "$match": [ + { "$eq": [ + { "$field": "$sme.Documents[].DocumentClassification.Class#value" }, + { "$strVal": "03-01" } + ] + }, + { "$eq": [ + { "$field": "$sme.Documents[].DocumentVersion.SMLLanguages#language" }, + { "$strVal": "nl" } + ] + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForDocumentsMatch(true); + QueryEvaluator evaluator = new QueryEvaluator(); + Submodel submodel = env.getSubmodels().get(0); + boolean result = evaluator.matches(query.get$condition(), submodel); + assertTrue(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void documentsMatch_withNonMatchingValues() throws Exception { + String json = """ + { + "$condition": { + "$match": [ + { "$eq": [ + { "$field": "$sme.Documents[].DocumentClassification.Class#value" }, + { "$strVal": "03-01" } + ] + }, + { "$eq": [ + { "$field": "$sme.Documents[].DocumentVersion.SMLLanguages#language" }, + { "$strVal": "nl" } + ] + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForDocumentsMatch(false); + QueryEvaluator evaluator = new QueryEvaluator(); + Submodel submodel = env.getSubmodels().get(0); + boolean result = evaluator.matches(query.get$condition(), submodel); + assertFalse(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void andMatch_withMatchingConditions() throws Exception { + String json = """ + { + "$condition": { + "$and": [ + { "$match": [ + { "$eq": [ + { "$field": "$sm#idShort" }, + { "$strVal": "TechnicalData" } + ] + }, + { "$eq": [ + { + "$field": + "$sme.ProductClassifications[].ProductClassId#value" + }, + { "$strVal": "27-37-09-05" } + ] + } + ] + }, + { "$match": [ + { + "$eq": [ + { "$field": "$sm#idShort" }, + { "$strVal": "TechnicalData" } + ] + }, + { + "$eq": [ + { "$field": "$sme#semanticId" }, + { "$strVal": "0173-1#02-BAF016#006" } + ] + }, + { + "$lt": [ + { "$field": "$sme#value" }, + { "$numVal": 100 } + ] + } + ] + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForAndMatch(true); + QueryEvaluator evaluator = new QueryEvaluator(); + Submodel submodel = env.getSubmodels().get(0); + boolean result = evaluator.matches(query.get$condition(), submodel); + assertTrue(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void andMatch_withNonMatchingConditions() throws Exception { + String json = """ + { + "$condition": { + "$and": [ + { "$match": [ + { "$eq": [ + { "$field": "$sm#idShort" }, + { "$strVal": "TechnicalData" } + ] + }, + { "$eq": [ + { + "$field": + "$sme.ProductClassifications[].ProductClassId#value" + }, + { "$strVal": "27-37-09-05" } + ] + } + ] + }, + { "$match": [ + { + "$eq": [ + { "$field": "$sm#idShort" }, + { "$strVal": "TechnicalData" } + ] + }, + { + "$eq": [ + { "$field": "$sme#semanticId" }, + { "$strVal": "0173-1#02-BAF016#006" } + ] + }, + { + "$lt": [ + { "$field": "$sme#value" }, + { "$numVal": 100 } + ] + } + ] + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForAndMatch(false); + QueryEvaluator evaluator = new QueryEvaluator(); + Submodel submodel = env.getSubmodels().get(0); + boolean result = evaluator.matches(query.get$condition(), submodel); + assertFalse(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void orMatch_withMatchingSpecificAssetIds() throws Exception { + String json = """ + { + "$condition": { + "$or": [ + { + "$match": [ + { "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].name" }, + { "$strVal": "supplierId" } + ] + }, + { "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].value" }, + { "$strVal": "aas-1" } + ] + } + ] + }, + { + "$match": [ + { + "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].name" }, + { "$strVal": "customerId" } + ] + }, + { + "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].value" }, + { "$strVal": "aas-2" } + ] + } + ] + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForOrMatch(true); + QueryEvaluator evaluator = new QueryEvaluator(); + AssetAdministrationShell aas = env.getAssetAdministrationShells().get(0); + boolean result = evaluator.matches(query.get$condition(), aas); + assertTrue(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void orMatch_withNonMatchingSpecificAssetIds() throws Exception { + String json = """ + { + "$condition": { + "$or": [ + { + "$match": [ + { "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].name" }, + { "$strVal": "supplierId" } + ] + }, + { "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].value" }, + { "$strVal": "aas-1" } + ] + } + ] + }, + { + "$match": [ + { + "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].name" }, + { "$strVal": "customerId" } + ] + }, + { + "$eq": [ + { "$field": "$aas#assetInformation.specificAssetIds[].value" }, + { "$strVal": "aas-2" } + ] + } + ] + } + ] + } + } + """; + + Query query = MAPPER.readValue( + json, new TypeReference<>() {}); + + Environment env = createTestEnvironmentForOrMatch(false); + QueryEvaluator evaluator = new QueryEvaluator(); + AssetAdministrationShell aas = env.getAssetAdministrationShells().get(0); + boolean result = evaluator.matches(query.get$condition(), aas); + assertFalse(result); + } + +} diff --git a/core/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/security/AccessRuleTest.java b/core/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/security/AccessRuleTest.java new file mode 100644 index 000000000..843e9ef6d --- /dev/null +++ b/core/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/security/AccessRuleTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.security; + +import static org.junit.Assert.assertNotNull; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.AccessPermissionRule; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Acl; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.AllAccessPermissionRules; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.AttributeItem; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.LogicalExpression; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.ObjectItem; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.RightsEnum; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import org.junit.Test; + + +public class AccessRuleTest { + + @Test + public void testAllowAnonymousReadConstruction() { + // Create ACL + Acl acl = new Acl(); + AttributeItem attr = new AttributeItem(); + attr.setGlobal(AttributeItem.Global.valueOf("ANONYMOUS")); + acl.setAttributes(Arrays.asList(attr)); + acl.setRights(Arrays.asList(RightsEnum.valueOf("READ"))); + acl.setAccess(Acl.Access.valueOf("ALLOW")); + + // Create OBJECTS + ObjectItem obj = new ObjectItem(); + obj.setRoute("*"); + + // Create FORMULA with a simple (boolean) expression + LogicalExpression formula = new LogicalExpression(); + formula.set$boolean(true); + + // Create a Rule + AccessPermissionRule rule = new AccessPermissionRule(); + rule.setAcl(acl); + rule.setObjects(Arrays.asList(obj)); + rule.setFormula(formula); + + // Wrap in AllAccessPermissionRules and the root + AllAccessPermissionRules allRules = new AllAccessPermissionRules(); + allRules.setRules(Arrays.asList(rule)); + assertNotNull(allRules.getRules()); + } + + + @Test + public void testParse() throws IOException { + InputStream inputStream = AccessRuleTest.class.getResourceAsStream("/ACLReadAccessAnonymous.json"); + if (inputStream == null) { + System.out.println("ACL not found in resources!"); + return; + } + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(inputStream); + AllAccessPermissionRules allRules = mapper.treeToValue(rootNode.get("AllAccessPermissionRules"), AllAccessPermissionRules.class); + assertNotNull(allRules); + } +} diff --git a/core/src/test/resources/ACLReadAccessAnonymous.json b/core/src/test/resources/ACLReadAccessAnonymous.json new file mode 100644 index 000000000..b84c98018 --- /dev/null +++ b/core/src/test/resources/ACLReadAccessAnonymous.json @@ -0,0 +1,27 @@ +{ + "AllAccessPermissionRules": { + "rules": [ + { + "ACL": { + "ATTRIBUTES": [ + { + "GLOBAL": "ANONYMOUS" + } + ], + "RIGHTS": [ + "READ" + ], + "ACCESS": "ALLOW" + }, + "OBJECTS": [ + { + "ROUTE": "*" + } + ], + "FORMULA": { + "$boolean": true + } + } + ] + } +} \ No newline at end of file diff --git a/docs/source/interfaces/endpoint.md b/docs/source/interfaces/endpoint.md index f3c0e931f..0ac224dc1 100644 --- a/docs/source/interfaces/endpoint.md +++ b/docs/source/interfaces/endpoint.md @@ -41,14 +41,18 @@ All endpoint implementations share the following common configuration properties The HTTP Endpoint allows accessing data and execute operations within the FA³ST Service via REST-API. In accordance to the specification, only HTTPS is supported since AAS v3.0. -The HTTP Endpoint is based on the document [Details of the Asset Administration Shell - Part 2: Application Programming Interfaces v3.0](https://industrialdigitaltwin.org/en/content-hub/aasspecifications/specification-of-the-asset-administration-shell-part-1-metamodel-idta-number-01001-3-0) and the corresponding [OpenAPI documentation v3.0.1](https://app.swaggerhub.com/apis/Plattform_i40/Entire-API-Collection/V3.0.1). +The HTTP Endpoint is based on the document [Details of the Asset Administration Shell - Part 2: Application Programming Interfaces v3.0](https://industrialdigitaltwin.org/en/content-hub/aasspecifications/specification-of-the-asset-administration-shell-part-2-application-programming-interfaces-idta-number-01002) and the corresponding [OpenAPI documentation v3.0.1](https://app.swaggerhub.com/apis/Plattform_i40/Entire-API-Collection). +Queries are supported if the chosen persistence implementation includes handling of queries. +This HTTP Endpoint also supports JWT Access Tokens and JSON Access Rules for Routes and Identifiables, as defined in [Part 4: Security (IDTA-01004)](https://industrialdigitaltwin.io/aas-specifications/IDTA-01004/v3.0.1/index.html). +Currently, semanticId fields in the rules are only validated for GET requests on /shells and /submodels, i.e. you can not grant POST access via a semanticId. Such access should be granted via Route/Identifiable. ### Configuration :::{table} Configuration properties of HTTP Endpoint. | Name | Allowed Value | Description | Default Value | | ------------------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -| certificate
*(optional)* | [CertificateInfo](#providing-certificates-in-configuration) | The HTTPS certificate to use.
| self-signed certificate | +| aclFolder
*(optional)* | String | Sets the path to the folder, where JSON Access Control Rules are defined | | +| certificate
*(optional)* | [CertificateInfo](#providing-certificates-in-configuration) | The HTTPS certificate to use.
| self-signed certificate | | corsAllowCredentials
*(optional)* | Boolean | Sets the `Access-Control-Allow-Credentials` response header. | false | | corsAllowedHeaders
*(optional)* | String (comma-separated list) | Sets the `Access-Control-Allow-Headers` response header. | * | | corsAllowedMethods
*(optional)* | String (comma-separated list) | Sets the `Access-Control-Allow-Methods` response header. | GET, POST, HEAD | @@ -57,7 +61,8 @@ The HTTP Endpoint is based on the document [Details of the Asset Administration | corsExposedHeaders
*(optional)* | String (comma-separated list) | Sets the `Access-Control-Expose-Headers` response header. | | | corsMaxAge
*(optional)* | Long | Sets the `Access-Control-Max-Age` response header. | 3600 | | hostname
*(optional)* | String | The hostname to be used for automatic registration with registry. | auto-detect (typically IP address) | -| pathPrefix
*(optional)* | String | The path prefix to be used for automatic registration with registry. Must start with a "/" and not end with a "/". Exceptions: "" and "/". (regex: `^(?:$|/|/.*[^/])$`) | /api/v3.0 | +| jwkProvider
*(optional)* | String | The URL of the IdentityProvider to verify JWT Access Tokens. | | +| pathPrefix
*(optional)* | String | The path prefix to be used for automatic registration with registry. Must start with a "/" and not end with a "/". (regex: `^\/.*[^\/]$`) | /api/v3.0 | | includeErrorDetails
*(optional)* | Boolean | If set, stack traceis added to the HTTP responses incase of error. | false | | port
*(optional)* | Integer | The port to use. | 443 | | sniEnabled
*(optional)* | Boolean | If Server Name Identification (SNI) should be enabled.
**This should only be disabled for testing purposes as it may present a security risk!** | true | @@ -70,6 +75,7 @@ The HTTP Endpoint is based on the document [Details of the Asset Administration { "endpoints": [ { "@class": "de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.HttpEndpoint", + "aclFolder": "C:\\Users\\FAST\\ACL", "certificate": { "keyStoreType": "PKCS12", "keyStorePath": "C:\faaast\MyKeyStore.p12", @@ -85,6 +91,7 @@ The HTTP Endpoint is based on the document [Details of the Asset Administration "corsExposedHeaders": "X-Custom-Header", "corsMaxAge": 1000, "hostname": "localhost", + "jwkProvider": "http://localhost:4444", "pathPrefix": "/api/v3.0", "includeErrorDetails": true, "port": 443, @@ -96,6 +103,126 @@ The HTTP Endpoint is based on the document [Details of the Asset Administration } ``` +### AAS Security Part 4 + +FA³ST Service supports the verification of JWT Access Tokens and simple Access rules. +If the configuration includes the `jwkProvider` URL, all HTTP API requests will be validated. +To grant anonymous READ access, an Access rule must be defined and placed in the `aclFolder`: + +``` +{ + "AllAccessPermissionRules": { + "rules": [ + { + "ACL": { + "ATTRIBUTES": [ + { + "GLOBAL": "ANONYMOUS" + } + ], + "RIGHTS": [ + "READ" + ], + "ACCESS": "ALLOW" + }, + "OBJECTS": [ + { + "ROUTE": "*" + } + ], + "FORMULA": { + "$boolean": true + } + } + ] + } +} +``` + +If a `jwkProvider` is defined and no Access rules exist, all HTTP API requests will be blocked. +We recommend Ory Hydra as OAuth2 and OpenID Connect server, which can be used with the following curl requests to retrieve a JWT token. + +Create client_id: +``` +curl -s -X POST http://127.0.0.1:4444/oauth2/token -u '6c176008-9308-4603-a55a-9975bb3a93b6:MCXepEOPh3GLx.hPpdb.NRCCsz' -d 'grant_type=client_credentials' -d 'scope=openid'" +``` +Retrieve token: +``` +curl -X POST -H "Content-Type: application/x-www-form-urlencoded" +-d "grant_type=client_credentials" -d "scope=openid" -d "client_id=6c176008-9308-4603-a55a-9975bb3a93b6" -d "client_secret=MCXepEOPh3GLx.hPpdb.NRCCsz" http://127.0.0.1:4444/oauth2/token +``` +To grant READ access to this client_id, the following Access rule can be used: +``` +{ + "AllAccessPermissionRules": { + "rules": [ + { + "ACL": { + "ATTRIBUTES": [ + { + "CLAIM": "client_id" + } + ], + "RIGHTS": [ + "READ" + ], + "ACCESS": "ALLOW" + }, + "OBJECTS": [ + { + "ROUTE": "*" + } + ], + "FORMULA": { + "$eq": [ + { + "$attribute": { + "CLAIM": "client_id" + } + }, + { + "$strVal": "6c176008-9308-4603-a55a-9975bb3a93b6" + } + ] + } + } + ] + } +} +``` + +To grant access to specific submodels, FA³ST Service supports Identifiables like `"IDENTIFIABLE": "(Submodel)https://example.com/ids/sm/5120_2111_9032_9005"` or `"IDENTIFIABLE": "(Submodel)*"`. +Example: +``` +{ + "AllAccessPermissionRules": { + "rules": [ + { + "ACL": { + "ATTRIBUTES": [ + { + "GLOBAL": "ANONYMOUS" + } + ], + "RIGHTS": [ + "READ" + ], + "ACCESS": "ALLOW" + }, + "OBJECTS": [ + { + "IDENTIFIABLE": "(Submodel)https://example.com/ids/sm/5120_2111_9032_9005" + } + ], + "FORMULA": { + "$boolean": true + } + } + ] + } +} +``` + ### API FA³ST Service supports the following APIs as defined by the [OpenAPI documentation v3.0.1](https://app.swaggerhub.com/apis/Plattform_i40/Entire-API-Collection/V3.0.1) diff --git a/endpoint/http/pom.xml b/endpoint/http/pom.xml index e95947c43..007fbd748 100644 --- a/endpoint/http/pom.xml +++ b/endpoint/http/pom.xml @@ -37,6 +37,16 @@ tests test + + com.auth0 + java-jwt + ${java.jwt.version} + + + com.auth0 + jwks-rsa + ${jwks.rsa.version} + com.fasterxml.jackson.core jackson-annotations @@ -85,6 +95,12 @@ ${org.apache.httpcomponents.client5.version} test + + org.awaitility + awaitility + ${awaitility.version} + test + org.eclipse.digitaltwin.aas4j aas4j-dataformat-core diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpoint.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpoint.java index 25b1f2ca7..f70f0fc3e 100644 --- a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpoint.java +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpoint.java @@ -16,18 +16,24 @@ import static de.fraunhofer.iosb.ilt.faaast.service.certificate.util.KeyStoreHelper.DEFAULT_ALIAS; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; import de.fraunhofer.iosb.ilt.faaast.service.certificate.CertificateData; import de.fraunhofer.iosb.ilt.faaast.service.certificate.CertificateInformation; import de.fraunhofer.iosb.ilt.faaast.service.certificate.util.KeyStoreHelper; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.AbstractEndpoint; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter.JwtValidationFilter; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.util.HttpHelper; import de.fraunhofer.iosb.ilt.faaast.service.exception.EndpointException; import de.fraunhofer.iosb.ilt.faaast.service.model.Interface; import de.fraunhofer.iosb.ilt.faaast.service.util.EncodingHelper; +import jakarta.servlet.DispatcherType; import java.io.File; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -35,6 +41,7 @@ import java.security.cert.CertificateException; import java.time.Duration; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -73,6 +80,12 @@ public class HttpEndpoint extends AbstractEndpoint { private static final String ENDPOINT_PROTOCOL = "HTTP"; private static final String ENDPOINT_PROTOCOL_VERSION = "1.1"; private Server server; + + @Override + public HttpEndpointConfig asConfig() { + return config; + } + private ServletContextHandler context; @Override @@ -91,6 +104,20 @@ public void start() throws EndpointException { RequestHandlerServlet handler = new RequestHandlerServlet(this, config, serviceContext); context.addServlet(handler, "/*"); + + if (Objects.nonNull(config.getJwkProvider())) { + URL jwkProviderUrl; + try { + jwkProviderUrl = new URL(config.getJwkProvider()); + } + catch (MalformedURLException malformedJwkProviderUrl) { + throw new EndpointException("Could not parse JWK provider URL", malformedJwkProviderUrl); + } + JwkProvider jwkProvider = new UrlJwkProvider(jwkProviderUrl); + + context.addFilter(new JwtValidationFilter(jwkProvider), + "*", EnumSet.allOf(DispatcherType.class)); + } server.setErrorHandler(new HttpErrorHandler(config)); try { server.start(); diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpointConfig.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpointConfig.java index 08052b9c3..6ca584b59 100644 --- a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpointConfig.java +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/HttpEndpointConfig.java @@ -59,6 +59,8 @@ public static Builder builder() { private int port; private boolean sniEnabled; private boolean sslEnabled; + private String jwkProvider; + private String aclFolder; public HttpEndpointConfig() { certificate = CertificateConfig.builder() @@ -225,6 +227,26 @@ public void setSslEnabled(boolean sslEnabled) { } + public String getJwkProvider() { + return jwkProvider; + } + + + public void setJwkProvider(String jwkProvider) { + this.jwkProvider = jwkProvider; + } + + + public String getAclFolder() { + return aclFolder; + } + + + public void setAclFolder(String aclFolder) { + this.aclFolder = aclFolder; + } + + @Override public boolean equals(Object o) { if (this == o) { @@ -249,6 +271,10 @@ public boolean equals(Object o) { && Objects.equals(port, that.port) && Objects.equals(sniEnabled, that.sniEnabled) && Objects.equals(sslEnabled, that.sslEnabled) + && Objects.equals(certificate, that.certificate) + && Objects.equals(hostname, that.hostname) + && Objects.equals(jwkProvider, that.jwkProvider) + && Objects.equals(aclFolder, that.aclFolder) && Objects.equals(profiles, that.profiles); } @@ -271,6 +297,8 @@ public int hashCode() { port, sniEnabled, sslEnabled, + jwkProvider, + aclFolder, profiles); } @@ -348,6 +376,12 @@ public B hostname(String value) { } + public B jwkProvider(String value) { + getBuildingInstance().setJwkProvider(value); + return getSelf(); + } + + public B pathPrefix(String value) { getBuildingInstance().setPathPrefix(value); return getSelf(); @@ -394,6 +428,12 @@ public B ssl(boolean value) { getBuildingInstance().setSslEnabled(value); return getSelf(); } + + + public B aclFolder(String value) { + getBuildingInstance().setAclFolder(value); + return getSelf(); + } } public static class Builder extends AbstractBuilder { diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/RequestHandlerServlet.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/RequestHandlerServlet.java index eda1059c5..61dcb0b61 100644 --- a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/RequestHandlerServlet.java +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/RequestHandlerServlet.java @@ -16,12 +16,17 @@ import de.fraunhofer.iosb.ilt.faaast.service.ServiceContext; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.exception.MethodNotAllowedException; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.exception.UnauthorizedException; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpMethod; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpRequest; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.request.RequestMappingManager; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.response.ResponseMappingManager; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter.ApiGateway; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.serialization.HttpJsonApiSerializer; import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.util.HttpHelper; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.aasrepository.GetAllAssetAdministrationShellsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.submodel.GetSubmodelResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.submodelrepository.GetAllSubmodelsResponse; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.InvalidRequestException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotFoundException; import de.fraunhofer.iosb.ilt.faaast.service.util.Ensure; @@ -51,6 +56,7 @@ public class RequestHandlerServlet extends HttpServlet { private final RequestMappingManager requestMappingManager; private final ResponseMappingManager responseMappingManager; private final HttpJsonApiSerializer serializer; + private final ApiGateway apiGateway; public RequestHandlerServlet(HttpEndpoint endpoint, HttpEndpointConfig config, ServiceContext serviceContext) { Ensure.requireNonNull(endpoint, "endpoint must be non-null"); @@ -62,6 +68,7 @@ public RequestHandlerServlet(HttpEndpoint endpoint, HttpEndpointConfig config, S this.requestMappingManager = new RequestMappingManager(serviceContext); this.responseMappingManager = new ResponseMappingManager(serviceContext); this.serializer = new HttpJsonApiSerializer(); + this.apiGateway = Objects.nonNull(config.getAclFolder()) ? new ApiGateway(config.getAclFolder()) : null; } @@ -97,7 +104,7 @@ protected void service(HttpServletRequest request, HttpServletResponse response) request::getHeader))) .build(); try { - executeAndSend(response, requestMappingManager.map(httpRequest)); + executeAndSend(request, response, requestMappingManager.map(httpRequest)); } catch (Exception e) { doThrow(e); @@ -120,12 +127,20 @@ private void checkRequestSupportedByProfiles(de.fraunhofer.iosb.ilt.faaast.servi } - private void executeAndSend(HttpServletResponse response, de.fraunhofer.iosb.ilt.faaast.service.model.api.Request apiRequest) throws Exception { + private void executeAndSend(HttpServletRequest request, HttpServletResponse response, + de.fraunhofer.iosb.ilt.faaast.service.model.api.Request apiRequest) + throws Exception { if (Objects.isNull(apiRequest)) { throw new InvalidRequestException("empty API request"); } checkRequestSupportedByProfiles(apiRequest); - de.fraunhofer.iosb.ilt.faaast.service.model.api.Response apiResponse = serviceContext.execute(endpoint, apiRequest); + de.fraunhofer.iosb.ilt.faaast.service.model.api.Response apiResponse = null; + if (Objects.nonNull(apiGateway)) { + apiResponse = handleResponseWithAcl(request, apiRequest); + } + else { + apiResponse = serviceContext.execute(endpoint, apiRequest); + } if (Objects.isNull(apiResponse)) { throw new ServletException("empty API response"); } @@ -149,4 +164,32 @@ private static boolean isSuccessful(de.fraunhofer.iosb.ilt.faaast.service.model. .noneMatch(x -> Objects.equals(x, MessageTypeEnum.ERROR) || Objects.equals(x, MessageTypeEnum.EXCEPTION)); } + + private de.fraunhofer.iosb.ilt.faaast.service.model.api.Response handleResponseWithAcl(HttpServletRequest request, + de.fraunhofer.iosb.ilt.faaast.service.model.api.Request apiRequest) + throws ServletException { + String url = request.getRequestURI(); + if ((url.equals("/shells") || url.equals("/shells/")) && request.getMethod().equals("GET")) { + GetAllAssetAdministrationShellsResponse aasResponse = (GetAllAssetAdministrationShellsResponse) serviceContext.execute(endpoint, apiRequest);; + return apiGateway.filterAas(request, aasResponse); + } + else if ((url.equals("/submodels/") || url.equals("/submodels")) && request.getMethod().equals("GET")) { + GetAllSubmodelsResponse submodelsResponse = (GetAllSubmodelsResponse) serviceContext.execute(endpoint, apiRequest);; + return apiGateway.filterSubmodels(request, submodelsResponse); + } + else if ((url.matches("^/submodels/[^/]+$")) && request.getMethod().equals("GET")) { + GetSubmodelResponse submodelResponse = (GetSubmodelResponse) serviceContext.execute(endpoint, apiRequest);; + if (!apiGateway.filterSubmodel(request, submodelResponse)) { + doThrow(new UnauthorizedException( + String.format("User not authorized '%s'", request.getRequestURI()))); + } + return submodelResponse; + } + else if (!apiGateway.isAuthorized(request)) { + doThrow(new UnauthorizedException( + String.format("User not authorized '%s'", request.getRequestURI()))); + } + return serviceContext.execute(endpoint, apiRequest); + } + } diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/exception/UnauthorizedException.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/exception/UnauthorizedException.java new file mode 100644 index 000000000..b05e52aab --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/exception/UnauthorizedException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.exception; + +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.InvalidRequestException; + + +/** + * Exception to indicate user is not authorized for the URL. + */ +public class UnauthorizedException extends InvalidRequestException { + + public UnauthorizedException(String message) { + super(message); + } + + + public UnauthorizedException(HttpRequest request) { + super(String.format("user not allowed for URL '%s'", + request.getPath())); + } +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/aasrepository/QueryAssetAdministrationShellsRequestMapper.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/aasrepository/QueryAssetAdministrationShellsRequestMapper.java new file mode 100644 index 000000000..f17b57cfc --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/aasrepository/QueryAssetAdministrationShellsRequestMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.request.mapper.aasrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.ServiceContext; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpMethod; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpRequest; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.request.mapper.AbstractRequestMapper; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.Request; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository.QueryAssetAdministrationShellsRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.InvalidRequestException; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; +import java.util.Map; + + +/** + * class to map HTTP-POST-Request path: query/shells. + */ +public class QueryAssetAdministrationShellsRequestMapper extends AbstractRequestMapper { + + private static final String PATTERN = "query/shells"; + + public QueryAssetAdministrationShellsRequestMapper(ServiceContext serviceContext) { + super(serviceContext, HttpMethod.POST, PATTERN); + } + + + @Override + public Request doParse(HttpRequest httpRequest, Map urlParameters) throws InvalidRequestException { + return QueryAssetAdministrationShellsRequest.builder() + .query(parseBody(httpRequest, Query.class)) + .build(); + } +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/conceptdescription/QueryConceptDescriptionsRequestMapper.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/conceptdescription/QueryConceptDescriptionsRequestMapper.java new file mode 100644 index 000000000..f8750434f --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/conceptdescription/QueryConceptDescriptionsRequestMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.request.mapper.conceptdescription; + +import de.fraunhofer.iosb.ilt.faaast.service.ServiceContext; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpMethod; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpRequest; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.request.mapper.AbstractRequestMapper; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.Request; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.QueryConceptDescriptionsRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.InvalidRequestException; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; +import java.util.Map; + + +/** + * class to map HTTP-POST-Request path: query/concept-descriptions. + */ +public class QueryConceptDescriptionsRequestMapper extends AbstractRequestMapper { + + private static final String PATTERN = "query/concept-descriptions"; + + public QueryConceptDescriptionsRequestMapper(ServiceContext serviceContext) { + super(serviceContext, HttpMethod.POST, PATTERN); + } + + + @Override + public Request doParse(HttpRequest httpRequest, Map urlParameters) throws InvalidRequestException { + return QueryConceptDescriptionsRequest.builder() + .query(parseBody(httpRequest, Query.class)) + .build(); + } +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/submodelrepository/QuerySubmodelsRequestMapper.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/submodelrepository/QuerySubmodelsRequestMapper.java new file mode 100644 index 000000000..59c398290 --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/request/mapper/submodelrepository/QuerySubmodelsRequestMapper.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.request.mapper.submodelrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.ServiceContext; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpMethod; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpRequest; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.request.mapper.AbstractRequestMapper; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.Request; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.submodelrepository.QuerySubmodelsRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.exception.InvalidRequestException; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; +import java.util.Map; + + +/** + * class to map HTTP-POST-Request path: query/submodels. + */ +public class QuerySubmodelsRequestMapper extends AbstractRequestMapper { + + private static final String PATTERN = "query/submodels"; + + public QuerySubmodelsRequestMapper(ServiceContext serviceContext) { + super(serviceContext, HttpMethod.POST, PATTERN); + } + + + @Override + public Request doParse(HttpRequest httpRequest, Map urlParameters) throws InvalidRequestException { + return QuerySubmodelsRequest.builder() + .query(parseBody(httpRequest, Query.class)) + .build(); + } +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/FormulaEvaluator.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/FormulaEvaluator.java new file mode 100644 index 000000000..e98263e77 --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/FormulaEvaluator.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security; + +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.AttributeItem; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.LogicalExpression; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.StringValue; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Value; +import java.time.LocalTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + + +/** + * Class to evaluate formulas. + */ +public final class FormulaEvaluator { + + /** + * Evaluates the given formula. + * + * @param formula The formula. + * @param runtimeValues The runtime values. + * @return True if the evaluation is successful, false otherwise. + */ + public static boolean evaluate(LogicalExpression formula, + Map runtimeValues) { + return eval(formula, runtimeValues); + } + + + private static boolean eval(LogicalExpression node, + Map ctx) { + if (!node.get$and().isEmpty()) { + for (LogicalExpression child: node.get$and()) { + if (!eval(child, ctx)) { + return false; + } + } + return true; + } + if (!node.get$or().isEmpty()) { + for (LogicalExpression child: node.get$or()) { + if (eval(child, ctx)) { + return true; + } + } + return false; + } + if (node.get$not() != null) { + return !eval(node.get$not(), ctx); + } + if (!node.get$eq().isEmpty()) { + List ops = node.get$eq(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$eq requires exactly 2 operands"); + } + Object left = resolve(ops.get(0), ctx); + Object right = resolve(ops.get(1), ctx); + return Objects.equals(left, right); + } + if (!node.get$ne().isEmpty()) { + List ops = node.get$ne(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$ne requires exactly 2 operands"); + } + Object left = resolve(ops.get(0), ctx); + Object right = resolve(ops.get(1), ctx); + return !Objects.equals(left, right); + } + if (!node.get$gt().isEmpty()) { + List ops = node.get$gt(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$gt requires exactly 2 operands"); + } + Object lObj = resolve(ops.get(0), ctx); + Object rObj = resolve(ops.get(1), ctx); + if (!(lObj instanceof Comparable) || !(rObj instanceof Comparable)) { + throw new IllegalArgumentException("Operands are not comparable: " + + lObj + ", " + rObj); + } + int cmp = ((Comparable) lObj).compareTo(rObj); + return cmp > 0; + } + if (!node.get$ge().isEmpty()) { + List ops = node.get$ge(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$ge requires exactly 2 operands"); + } + Object lObj = resolve(ops.get(0), ctx); + Object rObj = resolve(ops.get(1), ctx); + if (!(lObj instanceof Comparable) || !(rObj instanceof Comparable)) { + throw new IllegalArgumentException("Operands are not comparable: " + + lObj + ", " + rObj); + } + int cmp = ((Comparable) lObj).compareTo(rObj); + return cmp >= 0; + } + if (!node.get$lt().isEmpty()) { + List ops = node.get$lt(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$lt requires exactly 2 operands"); + } + Object lObj = resolve(ops.get(0), ctx); + Object rObj = resolve(ops.get(1), ctx); + if (!(lObj instanceof Comparable) || !(rObj instanceof Comparable)) { + throw new IllegalArgumentException("Operands are not comparable: " + + lObj + ", " + rObj); + } + int cmp = ((Comparable) lObj).compareTo(rObj); + return cmp < 0; + } + if (!node.get$le().isEmpty()) { + List ops = node.get$le(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$le requires exactly 2 operands"); + } + Object lObj = resolve(ops.get(0), ctx); + Object rObj = resolve(ops.get(1), ctx); + if (!(lObj instanceof Comparable) || !(rObj instanceof Comparable)) { + throw new IllegalArgumentException("Operands are not comparable: " + + lObj + ", " + rObj); + } + int cmp = ((Comparable) lObj).compareTo(rObj); + return cmp <= 0; + } + if (!node.get$contains().isEmpty()) { + List ops = node.get$contains(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$contains requires exactly 2 operands"); + } + String left = resolveString(ops.get(0), ctx); + String right = resolveString(ops.get(1), ctx); + return left != null && left.contains(right); + } + if (!node.get$startsWith().isEmpty()) { + List ops = node.get$startsWith(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$starts-with requires exactly 2 operands"); + } + String left = resolveString(ops.get(0), ctx); + String right = resolveString(ops.get(1), ctx); + return left != null && left.startsWith(right); + } + if (!node.get$endsWith().isEmpty()) { + List ops = node.get$endsWith(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$ends-with requires exactly 2 operands"); + } + String left = resolveString(ops.get(0), ctx); + String right = resolveString(ops.get(1), ctx); + return left != null && left.endsWith(right); + } + if (!node.get$regex().isEmpty()) { + List ops = node.get$regex(); + if (ops.size() != 2) { + throw new IllegalArgumentException("$regex requires exactly 2 operands"); + } + String left = resolveString(ops.get(0), ctx); + String regex = resolveString(ops.get(1), ctx); + return left != null && Pattern.matches(regex, left); + } + if (node.get$boolean() != null) { + return node.get$boolean(); + } + if (!node.get$match().isEmpty()) { + throw new UnsupportedOperationException("Operator $match not supported"); + } + throw new IllegalArgumentException("No supported operator found in node"); + } + + + private static Object resolve(Value operand, + Map ctx) { + if (operand.get$strVal() != null) { + return operand.get$strVal(); + } + if (operand.get$timeVal() != null) { + return LocalTime.parse(operand.get$timeVal()); + } + if (operand.get$field() != null) { + return ctx.get(operand.get$field()); + } + if (operand.get$attribute() != null) { + AttributeItem path = operand.get$attribute(); + if (!Objects.isNull(path.getClaim())) { + return ctx.get("CLAIM:" + path.getClaim()); + } + if (!Objects.isNull(path.getReference())) { + return ctx.get("REF:" + path.getReference()); + } + return ctx.get("UTCNOW"); + } + throw new IllegalArgumentException("Unresolvable operand " + operand); + } + + + private static String resolveString(StringValue operand, + Map ctx) { + if (operand.get$strVal() != null) { + return operand.get$strVal(); + } + if (operand.get$field() != null) { + Object val = ctx.get(operand.get$field()); + return val != null ? val.toString() : null; + } + if (operand.get$attribute() != null) { + AttributeItem path = operand.get$attribute(); + String key = null; + if (!Objects.isNull(path.getClaim())) { + key = "CLAIM:" + path.getClaim(); + } + else if (!Objects.isNull(path.getReference())) { + key = "REF:" + path.getReference(); + } + else { + key = "UTCNOW"; + } + if (key != null) { + Object val = ctx.get(key); + return val != null ? val.toString() : null; + } + } + throw new IllegalArgumentException("Unresolvable operand " + operand); + } +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/auth/AuthState.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/auth/AuthState.java new file mode 100644 index 000000000..561d97e5b --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/auth/AuthState.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.auth; + +/** + * Shows the current state of an incoming HTTP request. + */ +public enum AuthState { + ANONYMOUS, + AUTHENTICATED +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/ApiGateway.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/ApiGateway.java new file mode 100644 index 000000000..54e2245aa --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/ApiGateway.java @@ -0,0 +1,564 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter; + +import static de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.model.HttpMethod.POST; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.FormulaEvaluator; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.Response; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.aasrepository.GetAllAssetAdministrationShellsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.submodel.GetSubmodelResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.submodelrepository.GetAllSubmodelsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.AccessPermissionRule; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Acl; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.AllAccessPermissionRules; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.AttributeItem; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Defacl; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Defattribute; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Defformula; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Defobject; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.LogicalExpression; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.ObjectItem; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.RightsEnum; +import de.fraunhofer.iosb.ilt.faaast.service.util.EncodingHelper; +import jakarta.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.nio.file.WatchKey; +import java.nio.file.WatchService; +import java.time.Clock; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; +import org.slf4j.LoggerFactory; + + +/** + * Filters any incoming request with respect to the given ACL rules. + */ +public class ApiGateway { + + private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(ApiGateway.class); + private static final String BEARER_KWD = "Bearer"; + + private Map aclList; + private final String abortMessage = "Invalid ACL folder path, AAS Security will not enforce rules.)"; + private final String errorMessage = "Invalid ACL rule, skipping."; + + public ApiGateway(String aclFolder) { + initializeAclList(aclFolder); + monitorAclRules(aclFolder); + } + + + /** + * Checks if the user is authorized to receive the response of the request. + * + * @param request the HttpRequest + * @return true if authorized and ACL exists + */ + public boolean isAuthorized(HttpServletRequest request) { + String token = request.getHeader("Authorization"); + if (token == null) { + return AuthServer.filterRules(this.aclList, null, request); + } + else { + if (token.startsWith(BEARER_KWD + " ")) { + token = token.substring(BEARER_KWD.length() + 1).trim(); + } + DecodedJWT jwt = JWT.decode(token); + return AuthServer.filterRules(this.aclList, jwt.getClaims(), request); + } + } + + + /** + * Filters out AAS that the user is not authorized for. + * + * @param request the HttpRequest + * @param response the ApiResponse + * @return the ApiResponse with only allowed AAS + */ + public Response filterAas(HttpServletRequest request, GetAllAssetAdministrationShellsResponse response) { + response.getPayload().getContent() + .removeIf(aas -> aclList.values().stream() + .noneMatch(a -> a.getRules().stream() + .anyMatch(r -> AuthServer.evaluateRule(r, "/shells/" + EncodingHelper.base64Encode(aas.getId()), + request.getMethod(), extractClaims(request), a)))); + return response; + } + + + /** + * Filters out Submodels that the user is not authorized for. + * + * @param request the HttpRequest + * @param response the ApiResponse + * @return the ApiResponse with only allowed Submodels + */ + public Response filterSubmodels(HttpServletRequest request, GetAllSubmodelsResponse response) { + response.getPayload().getContent().removeIf(submodel -> { + String path = "/submodels/" + EncodingHelper.base64Encode(submodel.getId()); + String method = request.getMethod(); + Map claims = extractClaims(request); + + Map fieldCtx = new HashMap<>(); + if (submodel.getSemanticId() != null) { + fieldCtx.put("$sm#semanticId", submodel.getSemanticId().getKeys().get(0).getValue()); + } + + return aclList.values().stream() + .noneMatch(allAccess -> allAccess.getRules().stream().anyMatch(rule -> AuthServer.evaluateRule(rule, path, method, claims, allAccess, fieldCtx))); + }); + return response; + } + + + /** + * Filters out the Submodel that the user is not authorized for. + * + * @param request the HttpRequest + * @param response the ApiResponse + * @return true if user is authorized + */ + public boolean filterSubmodel(HttpServletRequest request, GetSubmodelResponse response) { + Submodel submodel = response.getPayload(); + if (Objects.isNull(submodel)) { + return true; + } + String path = "/submodels/" + EncodingHelper.base64Encode(submodel.getId()); + String method = request.getMethod(); + Map claims = extractClaims(request); + + Map fieldCtx = new HashMap<>(); + if (submodel.getSemanticId() != null) { + fieldCtx.put("$sm#semanticId", submodel.getSemanticId().getKeys().get(0).getValue()); + } + + return aclList.values().stream() + .anyMatch(allAccess -> allAccess.getRules().stream().anyMatch(rule -> AuthServer.evaluateRule(rule, path, method, claims, allAccess, fieldCtx))); + } + + + private Map extractClaims(HttpServletRequest request) { + String token = request.getHeader("Authorization"); + if (token == null) { + return null; + } + if (token.startsWith(BEARER_KWD + " ")) { + token = token.substring(BEARER_KWD.length() + 1).trim(); + } + try { + DecodedJWT jwt = JWT.decode(token); + return jwt.getClaims(); + } + catch (com.auth0.jwt.exceptions.JWTDecodeException e) { + return null; + } + } + + /** + * Simple whitelist AuthServer implementation that supports ANONYMOUS access, + * claims with simple eq formulas and route authorization. + * Access must be explicitly defined, otherwise it is blocked. + */ + public static class AuthServer { + private static final String apiPrefix = "/api/v3.0/"; + + /** + * Check all rules that explicitly allows the request. + * If a rule exists after all filters, true is returned + * + * @param claims the claims found in the token + * @param request the request coming in + * @return true if there is a valid rule + */ + private static boolean filterRules(Map aclList, Map claims, HttpServletRequest request) { + String requestPath = request.getRequestURI(); + String path = requestPath.startsWith(apiPrefix) ? requestPath.substring(apiPrefix.length()) : requestPath; + String method = request.getMethod(); + List relevantRules = aclList.values().stream() + .filter(a -> a.getRules().stream() + .anyMatch(r -> evaluateRule(r, path, method, claims, a))) + .toList(); + return !relevantRules.isEmpty(); + } + + + private static boolean verifyAllClaims(Map claims, AccessPermissionRule rule, AllAccessPermissionRules allAccess, Map fieldCtx) { + Acl acl = getAcl(rule, allAccess); + + boolean isAnonymous = getAttributes(acl, allAccess).stream() + .anyMatch(attr -> attr.getGlobal() != null && "ANONYMOUS".equals(attr.getGlobal().value())); + + List claimNames = getAttributes(acl, allAccess).stream() + .filter(attr -> attr.getGlobal() == null) + .map(AttributeItem::getClaim) + .filter(Objects::nonNull) + .toList(); + + // Build context + Map ctx = new HashMap<>(); + if (claims != null) { + for (String name: claimNames) { + Claim c = claims.get(name); + if (c != null) { + ctx.put("CLAIM:" + name, c.asString()); + } + } + } + // Add $sm#semanticId + if (fieldCtx != null && !fieldCtx.isEmpty()) { + ctx.putAll(fieldCtx); + } + ctx.put("UTCNOW", LocalTime.now(Clock.systemUTC())); + if (isAnonymous) { + return FormulaEvaluator.evaluate(getFormula(rule, allAccess), ctx); + } + return !ctx.entrySet().stream().filter(e -> e.getKey().startsWith("CLAIM:")).toList().isEmpty() && + FormulaEvaluator.evaluate(getFormula(rule, allAccess), ctx); + } + + + private static boolean evaluateRights(List aclRights, String method, String path) { + String requiredRight = isOperationRequest(method, path) ? "EXECUTE" : getRequiredRight(method); + return aclRights.contains(RightsEnum.ALL) || aclRights.contains(RightsEnum.valueOf(requiredRight)); + } + + + private static boolean isOperationRequest(String method, String path) { + // Requirements: POST and URL suffix: invoke, invoke-async, invoke/$value, invoke-async/$value + String cleanPath; + String[] pathParts = path.split("/"); + + if (pathParts.length > 1 && "$value".equals(pathParts[pathParts.length - 1])) { + cleanPath = pathParts[pathParts.length - 2]; + } + else { + cleanPath = pathParts[pathParts.length - 1]; + } + + return POST.name().equals(method) && ("invoke".equals(cleanPath) || "invoke-async".equals(cleanPath)); + } + + + private static String getRequiredRight(String method) { + return switch (method) { + case "GET" -> "READ"; + case "POST" -> "CREATE"; + case "PUT" -> "UPDATE"; + case "DELETE" -> "DELETE"; + default -> throw new IllegalArgumentException("Unsupported method: " + method); + }; + } + + + private static boolean checkIdentifiable(String path, String identifiable) { + //check submodel path + if (!(path.startsWith("/submodels") || path.startsWith("/shells"))) { + return false; + } + + if ("(Submodel)*".equals(identifiable)) { + return true; + } + else if (identifiable.startsWith("(Submodel)")) { + String id = identifiable.substring(10); + return path.contains(Objects.requireNonNull(EncodingHelper.base64Encode(id))); + } + if ("(AssetAdministrationShell)*".equals(identifiable)) { + return true; + } + else if (identifiable.startsWith("(AssetAdministrationShell)")) { + String id = identifiable.substring(26); + return path.contains(Objects.requireNonNull(EncodingHelper.base64Encode(id))); + } + return false; + } + + + private static boolean checkDescriptor(String path, String descriptor) { + if (descriptor.startsWith("(aasDesc)")) { + if (!path.startsWith("/shell-descriptors")) { + return false; + } + if ("(aasDesc)*".equals(descriptor)) { + return true; + } + else if (descriptor.startsWith("(aasDesc)")) { + String id = descriptor.substring(9); + return path.contains(Objects.requireNonNull(EncodingHelper.base64UrlEncode(id))); + } + } + else if (descriptor.startsWith("(smDesc)")) { + if (!path.startsWith("/submodel-descriptors")) { + return false; + } + if ("(smDesc)*".equals(descriptor)) { + return true; + } + else if (descriptor.startsWith("(smDesc)")) { + String id = descriptor.substring(8); + return path.contains(Objects.requireNonNull(EncodingHelper.base64UrlEncode(id))); + } + } + return false; + } + + + private static boolean evaluateRule(AccessPermissionRule rule, String path, String method, Map claims, AllAccessPermissionRules allAccess) { + return evaluateRule(rule, path, method, claims, allAccess, null); + } + + + private static boolean evaluateRule(AccessPermissionRule rule, String path, String method, Map claims, AllAccessPermissionRules allAccess, + Map fieldCtx) { + Acl acl = getAcl(rule, allAccess); + return acl != null && getAttributes(acl, allAccess) != null && acl.getRights() != null && getObjects(rule, allAccess) != null + && getObjects(rule, allAccess).stream().anyMatch(attr -> { + if (attr.getRoute() != null) { + return "*".equals(attr.getRoute()) || attr.getRoute().contains(path); + } + else if (attr.getIdentifiable() != null) { + return checkIdentifiable(path, attr.getIdentifiable()); + } + else if (attr.getDescriptor() != null) { + return checkDescriptor(path, attr.getDescriptor()); + } + else { + return false; + } + }) && "ALLOW".equals(acl.getAccess().value()) && evaluateRights(acl.getRights(), method, path) && verifyAllClaims(claims, rule, allAccess, fieldCtx); + } + } + + private void initializeAclList(String aclFolder) { + this.aclList = new HashMap<>(); + if (aclFolder == null + || aclFolder.trim().isEmpty() + || !new File(aclFolder.trim()).isDirectory()) { + LOGGER.error(abortMessage); + return; + } + File folder = new File(aclFolder.trim()); + File[] jsonFiles = folder.listFiles((dir, name) -> name.toLowerCase().endsWith(".json")); + ObjectMapper mapper = new ObjectMapper(); + if (jsonFiles != null) { + for (File file: jsonFiles) { + Path filePath = file.toPath(); + try { + String jsonContent = Files.readString(filePath); + JsonNode rootNode = mapper.readTree(jsonContent); + AllAccessPermissionRules allRules; + if (rootNode.has("AllAccessPermissionRules")) { + allRules = mapper.treeToValue(rootNode.get("AllAccessPermissionRules"), AllAccessPermissionRules.class); + } + else { + allRules = mapper.readValue(jsonContent, AllAccessPermissionRules.class); + } + aclList.put(filePath, allRules); + } + catch (IOException e) { + LOGGER.error(errorMessage); + } + } + } + } + + + private void monitorAclRules(String aclFolder) { + if (aclFolder == null + || aclFolder.trim().isEmpty() + || !new File(aclFolder.trim()).isDirectory()) { + LOGGER.error(abortMessage); + return; + } + Path folderToWatch = Paths.get(aclFolder); + WatchService watchService; + try { + watchService = FileSystems.getDefault().newWatchService(); + // Register the folder with the WatchService for CREATE and DELETE events + folderToWatch.register( + watchService, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE); + monitorLoop(watchService, folderToWatch); + } + catch (IOException e) { + LOGGER.error(errorMessage); + } + + } + + + private void monitorLoop(WatchService watchService, Path folderToWatch) { + ObjectMapper mapper = new ObjectMapper(); + Thread monitoringThread = new Thread(() -> { + while (!Thread.currentThread().isInterrupted()) { + WatchKey watchKey; + try { + watchKey = watchService.take(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); // restore interrupt status + LOGGER.warn("ACL monitoring thread interrupted", e); + break; // exit loop + } + for (WatchEvent event: watchKey.pollEvents()) { + WatchEvent.Kind kind = event.kind(); + Path filePath = (Path) event.context(); + Path absolutePath = folderToWatch.resolve(filePath).toAbsolutePath(); + // Check if the file is a JSON file + if (filePath.toString().toLowerCase().endsWith(".json")) { + if (kind == StandardWatchEventKinds.ENTRY_CREATE) { + try { + String jsonContent = Files.readString(absolutePath); + JsonNode rootNode = mapper.readTree(jsonContent); + AllAccessPermissionRules allRules; + if (rootNode.has("AllAccessPermissionRules")) { + allRules = mapper.treeToValue(rootNode.get("AllAccessPermissionRules"), AllAccessPermissionRules.class); + } + else { + allRules = mapper.readValue(jsonContent, AllAccessPermissionRules.class); + } + aclList.put(absolutePath, allRules); + } + catch (IOException e) { + LOGGER.error(errorMessage); + } + LOGGER.info("Added new ACL rule."); + } + else if (kind == StandardWatchEventKinds.ENTRY_DELETE) { + aclList.remove(absolutePath); + LOGGER.info("Removed ACL rule."); + } + } + } + boolean valid = watchKey.reset(); + if (!valid) { + LOGGER.info("WatchKey no longer valid; exiting."); + break; + } + } + }); + monitoringThread.start(); + } + + + private static Acl getAcl(AccessPermissionRule rule, AllAccessPermissionRules allAccess) { + if (rule.getAcl() != null) { + return rule.getAcl(); + } + else if (rule.getUseacl() != null) { + Optional acl = allAccess.getDefacls().stream() + .filter(a -> Objects.equals(a.getName(), rule.getUseacl())) + .findAny(); + if (acl.isPresent()) { + return acl.get().getAcl(); + } + else { + throw new IllegalArgumentException("DEFACL not found: " + rule.getUseacl()); + } + } + else { + throw new IllegalArgumentException("invalid rule: ACL or USEACL must be specified"); + } + } + + + private static List getAttributes(Acl acl, AllAccessPermissionRules allAccess) { + if ((acl.getAttributes() != null) && (!acl.getAttributes().isEmpty())) { + return acl.getAttributes(); + } + else if (acl.getUseattributes() != null) { + Optional attribute = allAccess.getDefattributes().stream() + .filter(a -> Objects.equals(a.getName(), acl.getUseattributes())) + .findAny(); + if (attribute.isPresent()) { + return attribute.get().getAttributes(); + } + else { + throw new IllegalArgumentException("DEFATTRIBUTES not found: " + acl.getUseattributes()); + } + } + else { + throw new IllegalArgumentException("invalid rule: ATTRIBUTES or USEATTRIBUTES must be specified"); + } + } + + + private static LogicalExpression getFormula(AccessPermissionRule rule, AllAccessPermissionRules allAccess) { + if (rule.getFormula() != null) { + return rule.getFormula(); + } + else if (rule.getUseformula() != null) { + Optional formula = allAccess.getDefformulas().stream() + .filter(a -> Objects.equals(a.getName(), rule.getUseformula())) + .findAny(); + if (formula.isPresent()) { + return formula.get().getFormula(); + } + else { + throw new IllegalArgumentException("DEFFORMULA not found: " + rule.getUseformula()); + } + } + else { + throw new IllegalArgumentException("invalid rule: FORMULA or USEFORMULA must be specified"); + } + } + + + private static List getObjects(AccessPermissionRule rule, AllAccessPermissionRules allAccess) { + if ((rule.getObjects() != null) && (!rule.getObjects().isEmpty())) { + return rule.getObjects(); + } + else if (rule.getUseobjects() != null) { + // We must collect all Defobjects in all Useobjects + List objectList = allAccess.getDefobjects().stream() + .filter(a -> rule.getUseobjects().contains(a.getName())) + .toList(); + if (objectList.isEmpty()) { + throw new IllegalArgumentException("DEFOBJECTS not found: " + rule.getUseobjects()); + } + else { + Set retval = new HashSet<>(); + for (Defobject item: objectList) { + retval.addAll(item.getObjects()); + } + return retval.stream().toList(); + } + } + else { + throw new IllegalArgumentException("invalid rule: OBJECTS or USEOBJECTS must be specified"); + } + } +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtAuthorizationFilter.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtAuthorizationFilter.java new file mode 100644 index 000000000..e51715de9 --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtAuthorizationFilter.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; + + +/** + * Abstract filter for HTTP requests with JWT headers. + */ +public abstract class JwtAuthorizationFilter implements Filter { + private static final String BEARER_KWD = "Bearer"; + + /** + * Extracts a JWT from an HTTP request by reading its Authorization header, + * checking the presence of the "Bearer" keyword and returning the decoded token. + * + * @param request An incoming HTTP request. + * + * @return The decoded JWT if present, else null + */ + protected DecodedJWT extractAndDecodeJwt(HttpServletRequest request) { + var authHeaderValue = request.getHeader("Authorization"); + + if (authHeaderValue == null || !authHeaderValue.startsWith(BEARER_KWD.concat(" "))) { + return null; + } + + // Remove "Bearer " + String token = authHeaderValue.substring(BEARER_KWD.length()).trim(); + + return JWT.decode(token); + } +} diff --git a/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtValidationFilter.java b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtValidationFilter.java new file mode 100644 index 000000000..b879ba505 --- /dev/null +++ b/endpoint/http/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtValidationFilter.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter; + +import com.auth0.jwk.InvalidPublicKeyException; +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkException; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.SignatureVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.auth.AuthState; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.interfaces.RSAPublicKey; +import java.util.Collections; +import org.slf4j.LoggerFactory; + + +/** + * Filters any incoming request by verifying its JWT if available. + * If no Authorization: Bearer <...> header is available, assumes an anonymous request. + */ +public class JwtValidationFilter extends JwtAuthorizationFilter { + + private static final String AUTHORIZATION_KWD = "Authorization"; + private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(JwtValidationFilter.class); + + private final JwkProvider jwkProvider; + + public JwtValidationFilter(JwkProvider jwkProvider) { + this.jwkProvider = jwkProvider; + } + + + /** + * If a bearer token (JWT) is passed as header, validates this token and blocks the request as unauthenticated. + * + * @param servletRequest the ServletRequest object contains the client's request + * @param servletResponse the ServletResponse object contains the filter's response + * @param filterChain the FilterChain for invoking the next filter or the resource + * @throws IOException could not write HttpResponse body OR exception in next filter steps + * @throws ServletException exception in next filter steps + */ + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, + ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + HttpServletResponse httpResponse = (HttpServletResponse) servletResponse; + + // If multiple Authorization headers are present, attackers could maybe use one for auth and one for claims + var authHeaders = httpRequest.getHeaders(AUTHORIZATION_KWD); + var authHeaderList = Collections.list(authHeaders); + if (authHeaderList.size() > 1) { + LOGGER.debug("Multiple authorization headers present! Not authorizing request."); + respondForbidden(httpResponse); + return; + } + + if (httpRequest.getHeader(AUTHORIZATION_KWD) == null) { + // No JWT in request, anonymous requestor + servletRequest.setAttribute("auth.state", AuthState.ANONYMOUS); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + + // Extract JWT + DecodedJWT jwt = extractAndDecodeJwt(httpRequest); + if (jwt == null || !validateJWT(jwt)) { + LOGGER.debug("Could not extract and validate JWT"); + respondForbidden(httpResponse); + } + else { + // Continue with the request as authenticated + servletRequest.setAttribute("auth.state", AuthState.AUTHENTICATED); + filterChain.doFilter(servletRequest, servletResponse); + } + } + + + private static void respondForbidden(HttpServletResponse httpResponse) throws IOException { + httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); + httpResponse.getWriter().write("Invalid token"); + } + + + private boolean validateJWT(DecodedJWT decodedJWT) { + // Your JWT validation logic here + Jwk jwk; + try { + jwk = jwkProvider.get(decodedJWT.getKeyId()); + } + catch (JwkException getJwkException) { + LOGGER.debug("Could not get JWK from JWT. Not authorizing request.", getJwkException); + return false; + } + Algorithm algorithm; + try { + algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null); + } + catch (InvalidPublicKeyException invalidPublicKeyException) { + LOGGER.debug("InvalidPublicKeyException when reading public key from JWT. Not authorizing request", invalidPublicKeyException); + return false; + } + + try { + algorithm.verify(decodedJWT); + } + catch (SignatureVerificationException signatureVerificationException) { + LOGGER.debug("Could not verify JWT using algorithm {}", algorithm.getName()); + return false; + } + JWTVerifier verifier = JWT.require(algorithm).build(); + + try { + verifier.verify(decodedJWT); + } + catch (JWTVerificationException verificationException) { + LOGGER.debug("Could not verify JWT"); + return false; + } + + return true; + } +} diff --git a/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/FormulaEvaluatorTest.java b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/FormulaEvaluatorTest.java new file mode 100644 index 000000000..8b9b22268 --- /dev/null +++ b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/FormulaEvaluatorTest.java @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.LogicalExpression; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; + + +/** + * Unit tests for {@link FormulaEvaluator}. + */ +public class FormulaEvaluatorTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /* ------------------------------------------------------------------ */ + @Test + public void complexFormula_withMatchingClaims() throws Exception { + String json = """ + { + "$and": [ + { + "$or": [ + { + "$eq": [ + { "$field": "$sm#semanticId" }, + { "$strVal": "SemanticID-Nameplate" } + ] + }, + { + "$eq": [ + { "$field": "$sm#semanticId" }, + { "$strVal": "SemanticID-TechnicalData" } + ] + } + ] + }, + { + "$or": [ + { + "$eq": [ + { "$attribute": { "CLAIM": "email" } }, + { "$strVal": "user1@company1.com" } + ] + }, + { + "$eq": [ + { "$attribute": { "CLAIM": "email" } }, + { "$strVal": "user2@company2.com" } + ] + } + ] + } + ] + } + """; + + LogicalExpression formula = MAPPER.readValue( + json, new TypeReference<>() {}); + + Map ctx = new HashMap<>(); + ctx.put("$sm#semanticId", "SemanticID-TechnicalData"); // matches 2nd $eq + ctx.put("CLAIM:email", "user2@company2.com"); // matches 2nd $eq + boolean result = FormulaEvaluator.evaluate(formula, ctx); + assertTrue(result); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void regexFormula_withNonMatchingEmail() throws Exception { + String json = """ + { + "$and": [ + { + "$or": [ + { + "$eq": [ + { "$field": "$sm#semanticId" }, + { "$strVal": "SemanticID-Nameplate" } + ] + }, + { + "$eq": [ + { "$field": "$sm#semanticId" }, + { "$strVal": "SemanticID-TechnicalData" } + ] + } + ] + }, + { + "$regex": [ + { "$attribute": { "CLAIM": "email" } }, + { "$strVal": "[\\\\w\\\\.]+'@company\\\\.com" } + ] + } + ] + } + """; + + LogicalExpression formula = MAPPER.readValue( + json, new TypeReference<>() {}); + Map ctx = new HashMap<>(); + ctx.put("$sm#semanticId", "SemanticID-TechnicalData"); + ctx.put("CLAIM:email", "other.user@other-company.org"); // does NOT match + assertFalse(FormulaEvaluator.evaluate(formula, ctx)); + } + + + /* ------------------------------------------------------------------ */ + @Test + public void fullFormula_allConditionsMet() throws Exception { + String json = """ + { + "$and": [ + { + "$or": [ + { + "$eq": [ + { "$field": "$sm#semanticId" }, + { "$strVal": "SemanticID-Nameplate" } + ] + }, + { + "$eq": [ + { "$field": "$sm#semanticId" }, + { "$strVal": "SemanticID-TechnicalData" } + ] + } + ] + }, + { + "$eq": [ + { "$attribute": { "CLAIM": "companyName" } }, + { "$strVal": "company1-name" } + ] + }, + { + "$regex": [ + { "$attribute": { "REFERENCE": "(Submodel)*#Id" } }, + { "$strVal": "^https://company1.com/.*$" } + ] + }, + { + "$ge": [ + { "$attribute": { "GLOBAL": "UTCNOW" } }, + { "$timeVal": "09:00" } + ] + }, + { + "$le": [ + { "$attribute": { "GLOBAL": "UTCNOW" } }, + { "$timeVal": "17:00" } + ] + } + ] + } + """; + + LogicalExpression formula = MAPPER.readValue( + json, new TypeReference<>() {}); + Map ctx = new HashMap<>(); + ctx.put("$sm#semanticId", "SemanticID-TechnicalData"); + ctx.put("CLAIM:companyName", "company1-name"); + ctx.put("REF:(Submodel)*#Id", "https://company1.com/id-0815"); + ctx.put("UTCNOW", LocalTime.of(10, 30)); // between 09:00 and 17:00 + assertTrue(FormulaEvaluator.evaluate(formula, ctx)); + } + + + @Test + public void testFormula_ConditionsNotMet() throws JsonProcessingException { + String json = """ + { + "$and": [ + { + "$or": [ + { + "$eq": [ + { + "$attribute": { + "CLAIM": "organization" + } + }, + { + "$strVal": "[MyCompany]" + } + ] + }, + { + "$eq": [ + { + "$attribute": { + "CLAIM": "organization" + } + }, + { + "$strVal": "Company2" + } + ] + } + ] + }, + { + "$or": [ + { + "$eq": [ + { + "$attribute": { + "CLAIM": "email" + } + }, + { + "$strVal": "bob@example.com" + } + ] + }, + { + "$eq": [ + { + "$attribute": { + "CLAIM": "email" + } + }, + { + "$strVal": "user2@company2.com" + } + ] + } + ] + } + ] + } + """; + LogicalExpression formula = MAPPER.readValue( + json, new TypeReference<>() {}); + Map ctx = new HashMap<>(); + ctx.put("CLAIM:organization", "Company2"); + //ctx.put("CLAIM:email", "user2@company2.com"); + assertFalse(FormulaEvaluator.evaluate(formula, ctx)); + } + +} diff --git a/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/ApiGatewayFilterTest.java b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/ApiGatewayFilterTest.java new file mode 100644 index 000000000..aa2a14699 --- /dev/null +++ b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/ApiGatewayFilterTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + + +public class ApiGatewayFilterTest extends JwtAuthorizationFilterTest { + + private static final String ACL_JSON = "{\n" + + " \"AllAccessPermissionRules\": {\n" + + " \"rules\": [{\n" + + " \"ACL\": {\n" + + " \"ATTRIBUTES\": [{ \"GLOBAL\": \"ANONYMOUS\" }],\n" + + " \"RIGHTS\": [\"READ\"],\n" + + " \"ACCESS\": \"ALLOW\"\n" + + " },\n" + + " \"OBJECTS\": [{ \"ROUTE\": \"*\" }],\n" + + " \"FORMULA\": { \"$boolean\": true }\n" + + " }]\n" + + " }\n" + + "}"; + + @Rule + public TemporaryFolder tmp = new TemporaryFolder(); + private Path aclDir; + private ApiGateway apiGateway; + + private static HttpServletRequest req(String method, String uri) { + HttpServletRequest r = mock(HttpServletRequest.class); + when(r.getMethod()).thenReturn(method); + when(r.getRequestURI()).thenReturn(uri); + return r; + } + + + @Test + public void anonymousAccessDependsOnAclFile() throws Exception { + + aclDir = tmp.newFolder("acl").toPath(); + apiGateway = new ApiGateway(aclDir.toString()); + + HttpServletRequest request = req("GET", "/api/v3.0/submodels"); + HttpServletResponse response = mockResponse(); + FilterChain filter = mockFilterChain(); + + assertFalse(apiGateway.isAuthorized(request)); + // Verify that request was blocked off + verify(filter, never()).doFilter(any(), any()); + Path rule = aclDir.resolve("allow.json"); + Path tmpRule = aclDir.resolve("allow.json.tmp"); + Files.writeString(tmpRule, ACL_JSON, StandardCharsets.UTF_8); + Files.move(tmpRule, rule, StandardCopyOption.ATOMIC_MOVE); + + await().atMost(5, SECONDS).pollInterval(100, MILLISECONDS).untilAsserted(() -> assertTrue(apiGateway.isAuthorized(request))); + + Files.delete(rule); + await().atMost(5, SECONDS).pollInterval(100, MILLISECONDS).untilAsserted(() -> assertFalse(apiGateway.isAuthorized(request))); + } +} diff --git a/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtAuthorizationFilterTest.java b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtAuthorizationFilterTest.java new file mode 100644 index 000000000..3b1a63493 --- /dev/null +++ b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtAuthorizationFilterTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.List; +import org.junit.Test; + + +public class JwtAuthorizationFilterTest { + + private JwtAuthorizationFilter testSubject = new JwtAuthorizationFilter() { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + // Intentionally left blank + } + }; + + @Test + public void testExtractAndDecodeJwt() { + // TODO + } + + + protected FilterChain mockFilterChain() { + return mock(FilterChain.class); + } + + + protected static HttpServletRequest mockRequest(String method, String uri, String jwt) { + HttpServletRequest r = mock(HttpServletRequest.class); + //when(r.getMethod()).thenReturn(method); + //when(r.getRequestURI()).thenReturn(uri); + when(r.getHeader("Authorization")).thenReturn("Bearer " + jwt); + when(r.getHeaders("Authorization")).thenReturn(Collections.enumeration(List.of("Bearer " + jwt))); + + return r; + } + + + protected static HttpServletResponse mockResponse() { + HttpServletResponse r = mock(HttpServletResponse.class); + return r; + } + +} diff --git a/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtValidationFilterTest.java b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtValidationFilterTest.java new file mode 100644 index 000000000..270b7190b --- /dev/null +++ b/endpoint/http/src/test/java/de/fraunhofer/iosb/ilt/faaast/service/endpoint/http/security/filter/JwtValidationFilterTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.endpoint.http.security.filter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import org.junit.Test; + + +public class JwtValidationFilterTest extends JwtAuthorizationFilterTest { + + private JwtValidationFilter filter; + + @Test + public void jwtIsVerified() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + + RSAPublicKey pub = (RSAPublicKey) kp.getPublic(); + RSAPrivateKey priv = (RSAPrivateKey) kp.getPrivate(); + + String kid = "unit-test-kid"; + + String jwt = JWT.create() + .withKeyId(kid) + .sign(Algorithm.RSA256(pub, priv)); + + Jwk jwk = mock(Jwk.class); + when(jwk.getPublicKey()).thenReturn(pub); + when(jwk.getId()).thenReturn(kid); + + JwkProvider mockJwkProvider = mock(UrlJwkProvider.class); + + when(mockJwkProvider.get(kid)).thenReturn(jwk); + + filter = new JwtValidationFilter(mockJwkProvider); + + HttpServletRequest request = mockRequest("GET", "/api/v3.0/submodels", jwt); + HttpServletResponse response = mockResponse(); + FilterChain filterChain = mockFilterChain(); + + filter.doFilter(request, response, filterChain); + + // The filter passed this request onto the next filter -> Did not block + verify(filterChain, times(1)).doFilter(any(), any()); + // The filter called the JWK provider to verify the request + verify(mockJwkProvider, times(1)).get(kid); + } + +} diff --git a/model/pom.xml b/model/pom.xml index de7ca6607..4f09dcba1 100644 --- a/model/pom.xml +++ b/model/pom.xml @@ -207,6 +207,25 @@ org.apache.maven.plugins maven-checkstyle-plugin + + org.jsonschema2pojo + jsonschema2pojo-maven-plugin + ${jsonschema2pojo.version} + + ${project.basedir}/src/main/resources/schema.json + de.fraunhofer.iosb.ilt.faaast.service.model.query.json + false + true + true + + + + + generate + + + + diff --git a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/ServiceSpecificationProfile.java b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/ServiceSpecificationProfile.java index 84a6085e3..c8123d546 100644 --- a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/ServiceSpecificationProfile.java +++ b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/ServiceSpecificationProfile.java @@ -42,6 +42,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository.GetAssetAdministrationShellByIdRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository.PostAssetAdministrationShellRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository.PutAssetAdministrationShellByIdRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository.QueryAssetAdministrationShellsRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasserialization.GenerateSerializationByIdsRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.DeleteConceptDescriptionByIdRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.GetAllConceptDescriptionsByDataSpecificationReferenceRequest; @@ -51,6 +52,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.GetConceptDescriptionByIdRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.PostConceptDescriptionRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.PutConceptDescriptionByIdRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription.QueryConceptDescriptionsRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.description.GetSelfDescriptionRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.proprietary.ImportRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.proprietary.ResetRequest; @@ -87,6 +89,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.submodelrepository.PatchSubmodelByIdRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.submodelrepository.PostSubmodelRequest; import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.submodelrepository.PutSubmodelByIdRequest; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.submodelrepository.QuerySubmodelsRequest; import java.util.Arrays; import java.util.List; @@ -108,6 +111,7 @@ public enum ServiceSpecificationProfile { GetAllSubmodelReferencesRequest.class, GetAssetAdministrationShellReferenceRequest.class, GetAssetAdministrationShellRequest.class, + QueryAssetAdministrationShellsRequest.class, GetAssetInformationRequest.class, GetThumbnailRequest.class, PostSubmodelReferenceRequest.class, @@ -150,6 +154,7 @@ public enum ServiceSpecificationProfile { GetAllSubmodelReferencesRequest.class, GetAssetAdministrationShellReferenceRequest.class, GetAssetAdministrationShellRequest.class, + QueryAssetAdministrationShellsRequest.class, GetAssetInformationRequest.class, GetThumbnailRequest.class, GetAllSubmodelElementsPathRequest.class, @@ -273,6 +278,7 @@ public enum ServiceSpecificationProfile { GetAllAssetAdministrationShellsByIdShortRequest.class, GetAllAssetAdministrationShellsReferenceRequest.class, GetAllAssetAdministrationShellsRequest.class, + QueryAssetAdministrationShellsRequest.class, GetAssetAdministrationShellByIdReferenceRequest.class, GetAssetAdministrationShellByIdRequest.class, PostAssetAdministrationShellRequest.class, @@ -293,6 +299,7 @@ public enum ServiceSpecificationProfile { GetAllSubmodelsBySemanticIdRequest.class, GetAllSubmodelsReferenceRequest.class, GetAllSubmodelsRequest.class, + QuerySubmodelsRequest.class, GetSubmodelByIdReferenceRequest.class, GetSubmodelByIdRequest.class, PatchSubmodelByIdRequest.class, @@ -340,6 +347,7 @@ public enum ServiceSpecificationProfile { GetAllAssetAdministrationShellsByIdShortRequest.class, GetAllAssetAdministrationShellsReferenceRequest.class, GetAllAssetAdministrationShellsRequest.class, + QueryAssetAdministrationShellsRequest.class, GetAssetAdministrationShellByIdReferenceRequest.class, GetAssetAdministrationShellByIdRequest.class, GetAllSubmodelReferencesRequest.class, @@ -351,6 +359,7 @@ public enum ServiceSpecificationProfile { GetAllSubmodelsBySemanticIdRequest.class, GetAllSubmodelsReferenceRequest.class, GetAllSubmodelsRequest.class, + QuerySubmodelsRequest.class, GetSubmodelByIdReferenceRequest.class, GetSubmodelByIdRequest.class, GetAllSubmodelElementsPathRequest.class, @@ -377,6 +386,7 @@ public enum ServiceSpecificationProfile { GetAllSubmodelsBySemanticIdRequest.class, GetAllSubmodelsReferenceRequest.class, GetAllSubmodelsRequest.class, + QuerySubmodelsRequest.class, GetSubmodelByIdReferenceRequest.class, GetSubmodelByIdRequest.class, PatchSubmodelByIdRequest.class, @@ -420,6 +430,7 @@ public enum ServiceSpecificationProfile { GetAllSubmodelsBySemanticIdRequest.class, GetAllSubmodelsReferenceRequest.class, GetAllSubmodelsRequest.class, + QuerySubmodelsRequest.class, GetSubmodelByIdReferenceRequest.class, GetSubmodelByIdRequest.class, GetAllSubmodelElementsPathRequest.class, @@ -445,6 +456,7 @@ public enum ServiceSpecificationProfile { GetAllConceptDescriptionsByIdShortRequest.class, GetAllConceptDescriptionsByIsCaseOfRequest.class, GetAllConceptDescriptionsRequest.class, + QueryConceptDescriptionsRequest.class, GetConceptDescriptionByIdRequest.class, PostConceptDescriptionRequest.class, PutConceptDescriptionByIdRequest.class, diff --git a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/aasrepository/QueryAssetAdministrationShellsRequest.java b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/aasrepository/QueryAssetAdministrationShellsRequest.java new file mode 100644 index 000000000..f4b742776 --- /dev/null +++ b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/aasrepository/QueryAssetAdministrationShellsRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.model.api.request.aasrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.model.api.Request; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.AbstractRequestWithModifierAndPaging; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.aasrepository.QueryAssetAdministrationShellsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; +import java.util.Objects; + + +/** + * Request class for QueryAssetAdministrationShells requests. + */ +public class QueryAssetAdministrationShellsRequest extends AbstractRequestWithModifierAndPaging { + + private Query query; + + public Query getQuery() { + return query; + } + + + public void setQuery(Query query) { + this.query = query; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QueryAssetAdministrationShellsRequest that = (QueryAssetAdministrationShellsRequest) o; + return super.equals(that) + && Objects.equals(query, that.query); + } + + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), query); + } + + + public static Builder builder() { + return new Builder(); + } + + public abstract static class AbstractBuilder> extends Request.AbstractBuilder { + + public B query(Query value) { + getBuildingInstance().setQuery(value); + return getSelf(); + } + } + + public static class Builder extends AbstractBuilder { + + @Override + protected Builder getSelf() { + return this; + } + + + @Override + protected QueryAssetAdministrationShellsRequest newBuildingInstance() { + return new QueryAssetAdministrationShellsRequest(); + } + } +} diff --git a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/conceptdescription/QueryConceptDescriptionsRequest.java b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/conceptdescription/QueryConceptDescriptionsRequest.java new file mode 100644 index 000000000..93253d426 --- /dev/null +++ b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/conceptdescription/QueryConceptDescriptionsRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.model.api.request.conceptdescription; + +import de.fraunhofer.iosb.ilt.faaast.service.model.api.Request; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.AbstractRequestWithModifierAndPaging; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.conceptdescription.QueryConceptDescriptionsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; +import java.util.Objects; + + +/** + * Request class for QueryConceptDescriptions requests. + */ +public class QueryConceptDescriptionsRequest extends AbstractRequestWithModifierAndPaging { + + private Query query; + + public Query getQuery() { + return query; + } + + + public void setQuery(Query query) { + this.query = query; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QueryConceptDescriptionsRequest that = (QueryConceptDescriptionsRequest) o; + return super.equals(that) + && Objects.equals(query, that.query); + } + + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), query); + } + + + public static Builder builder() { + return new Builder(); + } + + public abstract static class AbstractBuilder> extends Request.AbstractBuilder { + + public B query(Query value) { + getBuildingInstance().setQuery(value); + return getSelf(); + } + } + + public static class Builder extends AbstractBuilder { + + @Override + protected Builder getSelf() { + return this; + } + + + @Override + protected QueryConceptDescriptionsRequest newBuildingInstance() { + return new QueryConceptDescriptionsRequest(); + } + } +} diff --git a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/submodelrepository/QuerySubmodelsRequest.java b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/submodelrepository/QuerySubmodelsRequest.java new file mode 100644 index 000000000..b1737ab8e --- /dev/null +++ b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/request/submodelrepository/QuerySubmodelsRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.model.api.request.submodelrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.model.api.Request; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.request.AbstractRequestWithModifierAndPaging; +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.submodelrepository.QuerySubmodelsResponse; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; +import java.util.Objects; + + +/** + * Request class for QuerySubmodels requests. + */ +public class QuerySubmodelsRequest extends AbstractRequestWithModifierAndPaging { + + private Query query; + + public Query getQuery() { + return query; + } + + + public void setQuery(Query query) { + this.query = query; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QuerySubmodelsRequest that = (QuerySubmodelsRequest) o; + return super.equals(that) + && Objects.equals(query, that.query); + } + + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), query); + } + + + public static Builder builder() { + return new Builder(); + } + + public abstract static class AbstractBuilder> extends Request.AbstractBuilder { + + public B query(Query value) { + getBuildingInstance().setQuery(value); + return getSelf(); + } + } + + public static class Builder extends AbstractBuilder { + + @Override + protected Builder getSelf() { + return this; + } + + + @Override + protected QuerySubmodelsRequest newBuildingInstance() { + return new QuerySubmodelsRequest(); + } + } +} diff --git a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/aasrepository/QueryAssetAdministrationShellsResponse.java b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/aasrepository/QueryAssetAdministrationShellsResponse.java new file mode 100644 index 000000000..0e017497a --- /dev/null +++ b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/aasrepository/QueryAssetAdministrationShellsResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.model.api.response.aasrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.AbstractPagedResponse; +import org.eclipse.digitaltwin.aas4j.v3.model.AssetAdministrationShell; + + +/** + * Response class for QueryAssetAdministrationShells requests. + */ +public class QueryAssetAdministrationShellsResponse extends AbstractPagedResponse { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AbstractBuilder { + + @Override + protected Builder getSelf() { + return this; + } + + + @Override + protected QueryAssetAdministrationShellsResponse newBuildingInstance() { + return new QueryAssetAdministrationShellsResponse(); + } + } +} diff --git a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/conceptdescription/QueryConceptDescriptionsResponse.java b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/conceptdescription/QueryConceptDescriptionsResponse.java new file mode 100644 index 000000000..11e5f0bba --- /dev/null +++ b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/conceptdescription/QueryConceptDescriptionsResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.model.api.response.conceptdescription; + +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.AbstractPagedResponse; +import org.eclipse.digitaltwin.aas4j.v3.model.ConceptDescription; + + +/** + * Response class for QueryConceptDescriptions requests. + */ +public class QueryConceptDescriptionsResponse extends AbstractPagedResponse { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AbstractBuilder { + + @Override + protected Builder getSelf() { + return this; + } + + + @Override + protected QueryConceptDescriptionsResponse newBuildingInstance() { + return new QueryConceptDescriptionsResponse(); + } + } +} diff --git a/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/submodelrepository/QuerySubmodelsResponse.java b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/submodelrepository/QuerySubmodelsResponse.java new file mode 100644 index 000000000..e2ec5da5e --- /dev/null +++ b/model/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/model/api/response/submodelrepository/QuerySubmodelsResponse.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Fraunhofer IOSB, eine rechtlich nicht selbstaendige + * Einrichtung der Fraunhofer-Gesellschaft zur Foerderung der angewandten + * Forschung e.V. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.fraunhofer.iosb.ilt.faaast.service.model.api.response.submodelrepository; + +import de.fraunhofer.iosb.ilt.faaast.service.model.api.response.AbstractPagedResponse; +import org.eclipse.digitaltwin.aas4j.v3.model.Submodel; + + +/** + * Response class for QuerySubmodels requests. + */ +public class QuerySubmodelsResponse extends AbstractPagedResponse { + + public static Builder builder() { + return new Builder(); + } + + public static class Builder extends AbstractBuilder { + + @Override + protected Builder getSelf() { + return this; + } + + + @Override + protected QuerySubmodelsResponse newBuildingInstance() { + return new QuerySubmodelsResponse(); + } + } +} diff --git a/model/src/main/resources/schema.json b/model/src/main/resources/schema.json new file mode 100644 index 000000000..9362a95cd --- /dev/null +++ b/model/src/main/resources/schema.json @@ -0,0 +1,817 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Common JSON Schema for AAS Queries and Access Rules", + "description": "This schema contains all classes that are shared between the AAS Query Language and the AAS Access Rule Language.", + "definitions": { + "standardString": { + "type": "string", + "pattern": "^(?!\\$).*" + }, + "modelStringPattern": { + "type": "string", + "pattern": "^((?:\\$aas#(?:idShort|id|assetInformation\\.assetKind|assetInformation\\.assetType|assetInformation\\.globalAssetId|assetInformation\\.(?:specificAssetIds(\\[[0-9]*\\])(?:\\.(?:name|value|externalSubjectId(?:\\.type|\\.keys\\[\\d*\\](?:\\.(?:type|value))?)?)?)|submodels\\.(?:type|keys\\[\\d*\\](?:\\.(?:type|value))?))|submodels\\.(type|keys\\[\\d*\\](?:\\.(type|value))?)))|(?:\\$sm#(?:semanticId(?:\\.type|\\.keys\\[\\d*\\](?:\\.(type|value))?)?|idShort|id))|(?:\\$sme(?:\\.[a-zA-Z][a-zA-Z0-9_]*(\\[[0-9]*\\])?(?:\\.[a-zA-Z][a-zA-Z0-9_]*(\\[[0-9]*\\])?)*)?#(?:semanticId(?:\\.type|\\.keys\\[\\d*\\](?:\\.(type|value))?)?|idShort|value|valueType|language))|(?:\\$cd#(?:idShort|id))|(?:\\$aasdesc#(?:idShort|id|assetKind|assetType|globalAssetId|specificAssetIds(\\[[0-9]*\\])?(?:\\.(name|value|externalSubjectId(?:\\.type|\\.keys\\[\\d*\\](?:\\.(type|value))?)?)?)|endpoints(\\[[0-9]*\\])\\.(interface|protocolinformation\\.href)|submodelDescriptors(\\[[0-9]*\\])\\.(semanticId(?:\\.type|\\.keys\\[\\d*\\](?:\\.(type|value))?)?|idShort|id|endpoints(\\[[0-9]*\\])\\.(interface|protocolinformation\\.href))))|(?:\\$smdesc#(?:semanticId(?:\\.type|\\.keys\\[\\d*\\](?:\\.(type|value))?)?|idShort|id|endpoints(\\[[0-9]*\\])\\.(interface|protocolinformation\\.href))))$" + }, + "hexLiteralPattern": { + "type": "string", + "pattern": "^16#[0-9A-F]+$" + }, + "dateTimeLiteralPattern": { + "type": "string", + "format": "date-time" + }, + "timeLiteralPattern": { + "type": "string", + "pattern": "^[0-9][0-9]:[0-9][0-9](:[0-9][0-9])?$" + }, + "Value": { + "type": "object", + "properties": { + "$field": { + "$ref": "#/definitions/modelStringPattern" + }, + "$strVal": { + "$ref": "#/definitions/standardString" + }, + "$attribute": { + "$ref": "#/definitions/attributeItem" + }, + "$numVal": { + "type": "number" + }, + "$hexVal": { + "$ref": "#/definitions/hexLiteralPattern" + }, + "$dateTimeVal": { + "$ref": "#/definitions/dateTimeLiteralPattern" + }, + "$timeVal": { + "$ref": "#/definitions/timeLiteralPattern" + }, + "$boolean": { + "type": "boolean" + }, + "$strCast": { + "$ref": "#/definitions/Value" + }, + "$numCast": { + "$ref": "#/definitions/Value" + }, + "$hexCast": { + "$ref": "#/definitions/Value" + }, + "$boolCast": { + "$ref": "#/definitions/Value" + }, + "$dateTimeCast": { + "$ref": "#/definitions/Value" + }, + "$timeCast": { + "$ref": "#/definitions/Value" + }, + "$dayOfWeek": { + "$ref": "#/definitions/dateTimeLiteralPattern" + }, + "$dayOfMonth": { + "$ref": "#/definitions/dateTimeLiteralPattern" + }, + "$month": { + "$ref": "#/definitions/dateTimeLiteralPattern" + }, + "$year": { + "$ref": "#/definitions/dateTimeLiteralPattern" + } + }, + "oneOf": [ + { + "required": [ + "$field" + ] + }, + { + "required": [ + "$strVal" + ] + }, + { + "required": [ + "$attribute" + ] + }, + { + "required": [ + "$numVal" + ] + }, + { + "required": [ + "$hexVal" + ] + }, + { + "required": [ + "$dateTimeVal" + ] + }, + { + "required": [ + "$timeVal" + ] + }, + { + "required": [ + "$boolean" + ] + }, + { + "required": [ + "$strCast" + ] + }, + { + "required": [ + "$numCast" + ] + }, + { + "required": [ + "$hexCast" + ] + }, + { + "required": [ + "$boolCast" + ] + }, + { + "required": [ + "$dateTimeCast" + ] + }, + { + "required": [ + "$timeCast" + ] + }, + { + "required": [ + "$dayOfWeek" + ] + }, + { + "required": [ + "$dayOfMonth" + ] + }, + { + "required": [ + "$month" + ] + }, + { + "required": [ + "$year" + ] + } + ], + "additionalProperties": false + }, + "stringValue": { + "type": "object", + "properties": { + "$field": { + "$ref": "#/definitions/modelStringPattern" + }, + "$strVal": { + "$ref": "#/definitions/standardString" + }, + "$strCast": { + "$ref": "#/definitions/Value" + }, + "$attribute": { + "$ref": "#/definitions/attributeItem" + } + }, + "oneOf": [ + { + "required": [ + "$field" + ] + }, + { + "required": [ + "$strVal" + ] + }, + { + "required": [ + "$strCast" + ] + }, + { + "required": [ + "$attribute" + ] + } + ], + "additionalProperties": false + }, + "comparisonItems": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "$ref": "#/definitions/Value" + } + }, + "stringItems": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "$ref": "#/definitions/stringValue" + } + }, + "matchExpression": { + "type": "object", + "properties": { + "$match": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/matchExpression" + } + }, + "$eq": { + "$ref": "#/definitions/comparisonItems" + }, + "$ne": { + "$ref": "#/definitions/comparisonItems" + }, + "$gt": { + "$ref": "#/definitions/comparisonItems" + }, + "$ge": { + "$ref": "#/definitions/comparisonItems" + }, + "$lt": { + "$ref": "#/definitions/comparisonItems" + }, + "$le": { + "$ref": "#/definitions/comparisonItems" + }, + "$contains": { + "$ref": "#/definitions/stringItems" + }, + "$starts-with": { + "$ref": "#/definitions/stringItems" + }, + "$ends-with": { + "$ref": "#/definitions/stringItems" + }, + "$regex": { + "$ref": "#/definitions/stringItems" + }, + "$boolean": { + "type": "boolean" + } + }, + "oneOf": [ + { + "required": [ + "$eq" + ] + }, + { + "required": [ + "$ne" + ] + }, + { + "required": [ + "$gt" + ] + }, + { + "required": [ + "$ge" + ] + }, + { + "required": [ + "$lt" + ] + }, + { + "required": [ + "$le" + ] + }, + { + "required": [ + "$contains" + ] + }, + { + "required": [ + "$starts-with" + ] + }, + { + "required": [ + "$ends-with" + ] + }, + { + "required": [ + "$regex" + ] + }, + { + "required": [ + "$boolean" + ] + }, + { + "required": [ + "$match" + ] + } + ], + "additionalProperties": false + }, + "logicalExpression": { + "type": "object", + "properties": { + "$and": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/definitions/logicalExpression" + } + }, + "$match": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/matchExpression" + } + }, + "$or": { + "type": "array", + "minItems": 2, + "items": { + "$ref": "#/definitions/logicalExpression" + } + }, + "$not": { + "$ref": "#/definitions/logicalExpression" + }, + "$eq": { + "$ref": "#/definitions/comparisonItems" + }, + "$ne": { + "$ref": "#/definitions/comparisonItems" + }, + "$gt": { + "$ref": "#/definitions/comparisonItems" + }, + "$ge": { + "$ref": "#/definitions/comparisonItems" + }, + "$lt": { + "$ref": "#/definitions/comparisonItems" + }, + "$le": { + "$ref": "#/definitions/comparisonItems" + }, + "$contains": { + "$ref": "#/definitions/stringItems" + }, + "$starts-with": { + "$ref": "#/definitions/stringItems" + }, + "$ends-with": { + "$ref": "#/definitions/stringItems" + }, + "$regex": { + "$ref": "#/definitions/stringItems" + }, + "$boolean": { + "type": "boolean" + } + }, + "oneOf": [ + { + "required": [ + "$and" + ] + }, + { + "required": [ + "$or" + ] + }, + { + "required": [ + "$not" + ] + }, + { + "required": [ + "$eq" + ] + }, + { + "required": [ + "$ne" + ] + }, + { + "required": [ + "$gt" + ] + }, + { + "required": [ + "$ge" + ] + }, + { + "required": [ + "$lt" + ] + }, + { + "required": [ + "$le" + ] + }, + { + "required": [ + "$contains" + ] + }, + { + "required": [ + "$starts-with" + ] + }, + { + "required": [ + "$ends-with" + ] + }, + { + "required": [ + "$regex" + ] + }, + { + "required": [ + "$boolean" + ] + }, + { + "required": [ + "$match" + ] + } + ], + "additionalProperties": false + }, + "attributeItem": { + "oneOf": [ + { + "required": [ + "CLAIM" + ] + }, + { + "required": [ + "GLOBAL" + ] + }, + { + "required": [ + "REFERENCE" + ] + } + ], + "properties": { + "CLAIM": { + "type": "string" + }, + "GLOBAL": { + "type": "string", + "enum": [ + "LOCALNOW", + "UTCNOW", + "CLIENTNOW", + "ANONYMOUS" + ] + }, + "REFERENCE": { + "type": "string" + } + }, + "additionalProperties": false + }, + "objectItem": { + "oneOf": [ + { + "required": [ + "ROUTE" + ] + }, + { + "required": [ + "IDENTIFIABLE" + ] + }, + { + "required": [ + "REFERABLE" + ] + }, + { + "required": [ + "FRAGMENT" + ] + }, + { + "required": [ + "DESCRIPTOR" + ] + } + ], + "properties": { + "ROUTE": { + "type": "string" + }, + "IDENTIFIABLE": { + "type": "string" + }, + "REFERABLE": { + "type": "string" + }, + "FRAGMENT": { + "type": "string" + }, + "DESCRIPTOR": { + "type": "string" + } + }, + "additionalProperties": false + }, + "rightsEnum": { + "type": "string", + "enum": [ + "CREATE", + "READ", + "UPDATE", + "DELETE", + "EXECUTE", + "VIEW", + "ALL", + "TREE" + ], + "additionalProperties": false + }, + "ACL": { + "type": "object", + "properties": { + "ATTRIBUTES": { + "type": "array", + "items": { + "$ref": "#/definitions/attributeItem" + } + }, + "USEATTRIBUTES": { + "type": "string" + }, + "RIGHTS": { + "type": "array", + "items": { + "$ref": "#/definitions/rightsEnum" + } + }, + "ACCESS": { + "type": "string", + "enum": [ + "ALLOW", + "DISABLED" + ] + } + }, + "required": [ + "RIGHTS", + "ACCESS" + ], + "oneOf": [ + { + "required": [ + "ATTRIBUTES" + ] + }, + { + "required": [ + "USEATTRIBUTES" + ] + } + ], + "additionalProperties": false + }, + "AccessPermissionRule": { + "type": "object", + "properties": { + "ACL": { + "$ref": "#/definitions/ACL" + }, + "USEACL": { + "type": "string" + }, + "OBJECTS": { + "type": "array", + "items": { + "$ref": "#/definitions/objectItem" + }, + "additionalProperties": false + }, + "USEOBJECTS": { + "type": "array", + "items": { + "type": "string" + } + }, + "FORMULA": { + "$ref": "#/definitions/logicalExpression", + "additionalProperties": false + }, + "USEFORMULA": { + "type": "string" + }, + "FRAGMENT": { + "type": "string" + }, + "FILTER": { + "$ref": "#/definitions/logicalExpression", + "additionalProperties": false + }, + "USEFILTER": { + "type": "string" + } + }, + "allOf": [ + { "oneOf": [ { "required": ["ACL"] }, { "required": ["USEACL"] } ] }, + { "oneOf": [ { "required": ["OBJECTS"] }, { "required": ["USEOBJECTS"] } ] }, + { "oneOf": [ { "required": ["FORMULA"] }, { "required": ["USEFORMULA"] } ] } ], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "Query": { + "type": "object", + "properties": { + "$select": { + "type": "string", + "pattern": "^id$" + }, + "$condition": { + "$ref": "#/definitions/logicalExpression" + } + }, + "required": [ + "$condition" + ], + "additionalProperties": false + }, + "AllAccessPermissionRules": { + "type": "object", + "properties": { + "DEFATTRIBUTES": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "attributes": { + "type": "array", + "items": { + "$ref": "#/definitions/attributeItem" + } + } + }, + "required": [ + "name", + "attributes" + ], + "additionalProperties": false + } + }, + "DEFACLS": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "acl": { + "$ref": "#/definitions/ACL" + } + }, + "required": [ + "name", + "acl" + ], + "additionalProperties": false + } + }, + "DEFOBJECTS": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "objects": { + "type": "array", + "items": { + "$ref": "#/definitions/objectItem" + } + }, + "USEOBJECTS": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "name" + ], + "oneOf": [ + { + "required": [ + "objects" + ] + }, + { + "required": [ + "USEOBJECTS" + ] + } + ], + "additionalProperties": false + } + }, + "DEFFORMULAS": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "formula": { + "$ref": "#/definitions/logicalExpression" + } + }, + "required": [ + "name", + "formula" + ], + "additionalProperties": false + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/AccessPermissionRule" + } + } + }, + "required": [ + "rules" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "Query" + ] + }, + { + "required": [ + "AllAccessPermissionRules" + ] + } + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/persistence/file/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/file/PersistenceFile.java b/persistence/file/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/file/PersistenceFile.java index 12ccae8da..dfdcd87c3 100644 --- a/persistence/file/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/file/PersistenceFile.java +++ b/persistence/file/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/file/PersistenceFile.java @@ -37,6 +37,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.exception.PersistenceException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotAContainerElementException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotFoundException; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; import de.fraunhofer.iosb.ilt.faaast.service.persistence.AssetAdministrationShellSearchCriteria; import de.fraunhofer.iosb.ilt.faaast.service.persistence.ConceptDescriptionSearchCriteria; import de.fraunhofer.iosb.ilt.faaast.service.persistence.Persistence; @@ -178,12 +179,25 @@ public Page findAssetAdministrationShells(AssetAdminis } + @Override + public Page findAssetAdministrationShellsWithQuery(AssetAdministrationShellSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, + Query query) { + return persistence.findAssetAdministrationShellsWithQuery(criteria, modifier, paging, query); + } + + @Override public Page findSubmodels(SubmodelSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) { return persistence.findSubmodels(criteria, modifier, paging); } + @Override + public Page findSubmodelsWithQuery(SubmodelSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) throws PersistenceException { + return persistence.findSubmodelsWithQuery(criteria, modifier, paging, query); + } + + @Override public Page findSubmodelElements(SubmodelElementSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) throws ResourceNotFoundException { return persistence.findSubmodelElements(criteria, modifier, paging); @@ -196,6 +210,13 @@ public Page findConceptDescriptions(ConceptDescriptionSearch } + @Override + public Page findConceptDescriptionsWithQuery(ConceptDescriptionSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) + throws PersistenceException { + return persistence.findConceptDescriptionsWithQuery(criteria, modifier, paging, query); + } + + @Override public void save(AssetAdministrationShell assetAdministrationShell) { persistence.save(assetAdministrationShell); diff --git a/persistence/memory/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/memory/PersistenceInMemory.java b/persistence/memory/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/memory/PersistenceInMemory.java index 851c2cb97..a11e6fb80 100644 --- a/persistence/memory/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/memory/PersistenceInMemory.java +++ b/persistence/memory/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/memory/PersistenceInMemory.java @@ -31,6 +31,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.exception.PersistenceException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotAContainerElementException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotFoundException; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; import de.fraunhofer.iosb.ilt.faaast.service.model.visitor.AssetAdministrationShellElementWalker; import de.fraunhofer.iosb.ilt.faaast.service.model.visitor.DefaultAssetAdministrationShellElementVisitor; import de.fraunhofer.iosb.ilt.faaast.service.persistence.AssetAdministrationShellSearchCriteria; @@ -39,6 +40,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.persistence.SubmodelElementSearchCriteria; import de.fraunhofer.iosb.ilt.faaast.service.persistence.SubmodelSearchCriteria; import de.fraunhofer.iosb.ilt.faaast.service.persistence.util.QueryModifierHelper; +import de.fraunhofer.iosb.ilt.faaast.service.query.QueryEvaluator; import de.fraunhofer.iosb.ilt.faaast.service.util.CollectionHelper; import de.fraunhofer.iosb.ilt.faaast.service.util.DeepCopyHelper; import de.fraunhofer.iosb.ilt.faaast.service.util.ElementValueHelper; @@ -215,6 +217,28 @@ public Page findAssetAdministrationShells(AssetAdminis } + @Override + public Page findAssetAdministrationShellsWithQuery(AssetAdministrationShellSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, + Query query) { + Ensure.requireNonNull(criteria, MSG_CRITERIA_NOT_NULL); + Ensure.requireNonNull(modifier, MSG_MODIFIER_NOT_NULL); + Ensure.requireNonNull(paging, MSG_PAGING_NOT_NULL); + + Stream result = environment.getAssetAdministrationShells().stream(); + if (criteria.isIdShortSet()) { + result = filterByIdShort(result, criteria.getIdShort()); + } + if (criteria.isAssetIdsSet()) { + result = filterByAssetIds(result, criteria.getAssetIds()); + } + QueryEvaluator evaluator = new QueryEvaluator(); + if (query != null) { + result = result.filter(aas -> evaluator.matches(query.get$condition(), aas)); + } + return preparePagedResult(result, modifier, paging); + } + + @Override public Page findConceptDescriptions(ConceptDescriptionSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) { Ensure.requireNonNull(criteria, MSG_CRITERIA_NOT_NULL); @@ -234,6 +258,30 @@ public Page findConceptDescriptions(ConceptDescriptionSearch } + @Override + public Page findConceptDescriptionsWithQuery(ConceptDescriptionSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) + throws PersistenceException { + Ensure.requireNonNull(criteria, MSG_CRITERIA_NOT_NULL); + Ensure.requireNonNull(modifier, MSG_MODIFIER_NOT_NULL); + Ensure.requireNonNull(paging, MSG_PAGING_NOT_NULL); + Stream result = environment.getConceptDescriptions().stream(); + if (criteria.isIdShortSet()) { + result = filterByIdShort(result, criteria.getIdShort()); + } + if (criteria.isIsCaseOfSet()) { + result = filterByIsCaseOf(result, criteria.getIsCaseOf()); + } + if (criteria.isDataSpecificationSet()) { + result = filterByDataSpecification(result, criteria.getDataSpecification()); + } + QueryEvaluator evaluator = new QueryEvaluator(); + if (query != null) { + result = result.filter(aas -> evaluator.matches(query.get$condition(), aas)); + } + return preparePagedResult(result, modifier, paging); + } + + @Override public Page findSubmodelElements(SubmodelElementSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) throws ResourceNotFoundException { Ensure.requireNonNull(criteria, MSG_CRITERIA_NOT_NULL); @@ -290,6 +338,26 @@ public Page findSubmodels(SubmodelSearchCriteria criteria, QueryModifi } + @Override + public Page findSubmodelsWithQuery(SubmodelSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) throws PersistenceException { + Ensure.requireNonNull(criteria, MSG_CRITERIA_NOT_NULL); + Ensure.requireNonNull(modifier, MSG_MODIFIER_NOT_NULL); + Ensure.requireNonNull(paging, MSG_PAGING_NOT_NULL); + Stream result = environment.getSubmodels().stream(); + if (criteria.isIdShortSet()) { + result = filterByIdShort(result, criteria.getIdShort()); + } + if (criteria.isSemanticIdSet()) { + result = filterBySemanticId(result, criteria.getSemanticId()); + } + QueryEvaluator evaluator = new QueryEvaluator(); + if (query != null) { + result = result.filter(aas -> evaluator.matches(query.get$condition(), aas)); + } + return preparePagedResult(result, modifier, paging); + } + + @Override public AssetAdministrationShell getAssetAdministrationShell(String id, QueryModifier modifier) throws ResourceNotFoundException { return prepareResult( diff --git a/persistence/mongo/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/mongo/PersistenceMongo.java b/persistence/mongo/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/mongo/PersistenceMongo.java index 4b6216c69..6e463a94c 100644 --- a/persistence/mongo/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/mongo/PersistenceMongo.java +++ b/persistence/mongo/src/main/java/de/fraunhofer/iosb/ilt/faaast/service/persistence/mongo/PersistenceMongo.java @@ -54,6 +54,7 @@ import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotAContainerElementException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.ResourceNotFoundException; import de.fraunhofer.iosb.ilt.faaast.service.model.exception.UnsupportedModifierException; +import de.fraunhofer.iosb.ilt.faaast.service.model.query.json.Query; import de.fraunhofer.iosb.ilt.faaast.service.persistence.AssetAdministrationShellSearchCriteria; import de.fraunhofer.iosb.ilt.faaast.service.persistence.ConceptDescriptionSearchCriteria; import de.fraunhofer.iosb.ilt.faaast.service.persistence.Persistence; @@ -256,6 +257,14 @@ public Page findAssetAdministrationShells(AssetAdminis } + @Override + public Page findAssetAdministrationShellsWithQuery(AssetAdministrationShellSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, + Query query) + throws PersistenceException { + throw new PersistenceException("Query not supported with mongoDB."); + } + + @Override public Page findConceptDescriptions(ConceptDescriptionSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) throws PersistenceException { Ensure.requireNonNull(criteria, MSG_CRITERIA_NOT_NULL); @@ -272,6 +281,13 @@ public Page findConceptDescriptions(ConceptDescriptionSearch } + @Override + public Page findConceptDescriptionsWithQuery(ConceptDescriptionSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) + throws PersistenceException { + throw new PersistenceException("Query not supported with mongoDB."); + } + + @Override public Page findSubmodels(SubmodelSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) throws PersistenceException { Ensure.requireNonNull(criteria, MSG_CRITERIA_NOT_NULL); @@ -286,6 +302,12 @@ public Page findSubmodels(SubmodelSearchCriteria criteria, QueryModifi } + @Override + public Page findSubmodelsWithQuery(SubmodelSearchCriteria criteria, QueryModifier modifier, PagingInfo paging, Query query) throws PersistenceException { + throw new PersistenceException("Query not supported with mongoDB."); + } + + @Override public Page findSubmodelElements(SubmodelElementSearchCriteria criteria, QueryModifier modifier, PagingInfo paging) throws ResourceNotFoundException, PersistenceException { diff --git a/pom.xml b/pom.xml index dfd823e18..6d22fbd61 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,7 @@ 2.1.2 4.0.4 3.1.12 + 4.4.0 2.3.1 5.6.0 12.1.5 @@ -95,7 +96,9 @@ 2.6.0 1.5.3 2.10.0 + 1.2.2 4.13.2 + 0.22.1 1.5.22 17 17