From 2db00772fed60900cd4c35b5af08e9a8dbb1f5c5 Mon Sep 17 00:00:00 2001 From: Aaron Zielstorff Date: Wed, 11 Mar 2026 09:27:27 +0100 Subject: [PATCH 1/5] Tries to fix incorrect supplementalSemanticIds --- .../resources/authorization/AasDescriptorSimple_2.json | 2 +- .../resources/authorization/SingleSubmodelDescriptor.json | 2 +- .../authorization/SingleSubmodelDescriptor_Update.json | 2 +- .../service/tests/integration/BaseIntegrationTest.java | 2 +- .../resources/authorization/AasDescriptorSimple_2.json | 2 +- .../resources/authorization/SingleSubmodelDescriptor.json | 2 +- .../authorization/SingleSubmodelDescriptor_Update.json | 2 +- .../resources/authorization/AasDescriptorSimple_2.json | 2 +- .../resources/authorization/SingleSubmodelDescriptor.json | 2 +- .../authorization/SingleSubmodelDescriptor_Update.json | 2 +- .../resources/authorization/AasDescriptorSimple_2.json | 2 +- .../resources/authorization/SingleSubmodelDescriptor.json | 2 +- .../authorization/SingleSubmodelDescriptor_Update.json | 2 +- .../resources/authorization/AasDescriptorSimple_2.json | 2 +- .../resources/authorization/SingleSubmodelDescriptor.json | 2 +- .../authorization/SingleSubmodelDescriptor_Update.json | 2 +- ...istryServiceSpecification-V3.0.1_SSP-001-resolved.yaml | 2 +- ...erviceSpecification-V3.0_SSP-001-resolved-altered.yaml | 2 +- ...egistryServiceSpecification-V3.0_SSP-001-resolved.yaml | 2 +- .../client/factory/SubmodelDescriptorFactory.java | 6 +++--- .../submodelregistry/client/mapper/AttributeMapper.java | 8 ++++---- .../client/mapper/TestAttributeMapper.java | 4 ++-- .../authorization/SubmodelDescriptorSimple_1.json | 2 +- .../authorization/SubmodelDescriptorSimple_2.json | 2 +- .../service/tests/integration/BaseIntegrationTest.java | 2 +- .../authorization/SubmodelDescriptorSimple_1.json | 2 +- .../authorization/SubmodelDescriptorSimple_2.json | 2 +- .../authorization/SubmodelDescriptorSimple_1.json | 2 +- .../authorization/SubmodelDescriptorSimple_2.json | 2 +- .../authorization/SubmodelDescriptorSimple_1.json | 2 +- .../authorization/SubmodelDescriptorSimple_2.json | 2 +- .../authorization/SubmodelDescriptorSimple_1.json | 2 +- .../authorization/SubmodelDescriptorSimple_2.json | 2 +- ...istryServiceSpecification-V3.0.1_SSP-001-resolved.yaml | 2 +- 34 files changed, 40 insertions(+), 40 deletions(-) diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_2.json index 423a700a3..5d22b649c 100644 --- a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_2.json +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -32,7 +32,7 @@ "idShort": "specificAasIdShort-2-SM", "id": "specificAasId-2-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor.json index 80e3c4d15..9086b0793 100644 --- a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor.json +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-SM", "id": "dummyShellId_3-1-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json index 338719502..c2e40f757 100644 --- a/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json +++ b/basyx.aasregistry/basyx.aasregistry-feature-authorization/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-Update-SM", "id": "dummyShellId_3-1-Update-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java index f5503018c..6708a2ed8 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java +++ b/basyx.aasregistry/basyx.aasregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/integration/BaseIntegrationTest.java @@ -567,7 +567,7 @@ public void whenSendFullObjectStructure_ItemIsProcessedProperly() throws ApiExce EmbeddedDataSpecification edSpec = new EmbeddedDataSpecification().dataSpecification(reference).dataSpecificationContent(new DataSpecificationContent(dsContent)); AdministrativeInformation aInfo = new AdministrativeInformation().addEmbeddedDataSpecificationsItem(edSpec); - SubmodelDescriptor sm = new SubmodelDescriptor().id("sm").id("short").addDescriptionItem(dType).addDisplayNameItem(nType).addEndpointsItem(ep).addExtensionsItem(ext).addSupplementalSemanticIdItem(reference); + SubmodelDescriptor sm = new SubmodelDescriptor().id("sm").id("short").addDescriptionItem(dType).addDisplayNameItem(nType).addEndpointsItem(ep).addExtensionsItem(ext).addSupplementalSemanticIdsItem(reference); AssetAdministrationShellDescriptor descriptor = new AssetAdministrationShellDescriptor().id("id1").id("short").addDescriptionItem(dType).addDisplayNameItem(nType).addEndpointsItem(ep).addExtensionsItem(ext) .addSpecificAssetIdsItem(saId).administration(aInfo).assetKind(AssetKind.TYPE).assetType("tp1").globalAssetId("global1").addSubmodelDescriptorsItem(sm); diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_2.json index 423a700a3..5d22b649c 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_2.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -32,7 +32,7 @@ "idShort": "specificAasIdShort-2-SM", "id": "specificAasId-2-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json index 80e3c4d15..9086b0793 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-SM", "id": "dummyShellId_3-1-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json index 338719502..c2e40f757 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-Update-SM", "id": "dummyShellId_3-1-Update-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json index 423a700a3..5d22b649c 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -32,7 +32,7 @@ "idShort": "specificAasIdShort-2-SM", "id": "specificAasId-2-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json index 80e3c4d15..9086b0793 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-SM", "id": "dummyShellId_3-1-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json index 338719502..c2e40f757 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-kafka-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-Update-SM", "id": "dummyShellId_3-1-Update-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_2.json index 423a700a3..5d22b649c 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_2.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -32,7 +32,7 @@ "idShort": "specificAasIdShort-2-SM", "id": "specificAasId-2-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json index 80e3c4d15..9086b0793 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-SM", "id": "dummyShellId_3-1-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json index 338719502..c2e40f757 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mem/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-Update-SM", "id": "dummyShellId_3-1-Update-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json index 423a700a3..5d22b649c 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/AasDescriptorSimple_2.json @@ -32,7 +32,7 @@ "idShort": "specificAasIdShort-2-SM", "id": "specificAasId-2-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json index 80e3c4d15..9086b0793 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-SM", "id": "dummyShellId_3-1-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json index 338719502..c2e40f757 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json +++ b/basyx.aasregistry/basyx.aasregistry-service-release-log-mongodb/src/test/resources/authorization/SingleSubmodelDescriptor_Update.json @@ -6,7 +6,7 @@ "idShort": "dummyShellIdShort_3-1-Update-SM", "id": "dummyShellId_3-1-Update-SM", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml b/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml index 1ab6c30e0..7f5e1b693 100644 --- a/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml +++ b/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml @@ -1192,7 +1192,7 @@ components: type: string semanticId: $ref: '#/components/schemas/Reference' - supplementalSemanticId: + supplementalSemanticIds: minItems: 1 type: array items: diff --git a/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved-altered.yaml b/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved-altered.yaml index 3f13a5de7..1021064f9 100644 --- a/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved-altered.yaml +++ b/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved-altered.yaml @@ -1194,7 +1194,7 @@ components: type: string semanticId: $ref: '#/components/schemas/Reference' - supplementalSemanticId: + supplementalSemanticIds: minItems: 1 type: array items: diff --git a/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved.yaml b/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved.yaml index f3347c120..bfc3fe7de 100644 --- a/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved.yaml +++ b/basyx.aasregistry/open-api/Plattform_i40-AssetAdministrationShellRegistryServiceSpecification-V3.0_SSP-001-resolved.yaml @@ -1185,7 +1185,7 @@ components: type: string semanticId: $ref: '#/components/schemas/Reference' - supplementalSemanticId: + supplementalSemanticIds: minItems: 1 type: array items: diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java index 3506e59fe..e17e36e98 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/factory/SubmodelDescriptorFactory.java @@ -86,7 +86,7 @@ public SubmodelDescriptor create(Submodel submodel) { setSemanticId(submodel.getSemanticId(), descriptor); - setSupplementalSemanticId(submodel.getSupplementalSemanticIds(), descriptor); + setSupplementalSemanticIds(submodel.getSupplementalSemanticIds(), descriptor); return descriptor; } @@ -131,12 +131,12 @@ private void setSemanticId(Reference reference, SubmodelDescriptor descriptor) { descriptor.setSemanticId(attributeMapper.mapSemanticId(reference)); } - private void setSupplementalSemanticId(List supplementalSemanticIds, SubmodelDescriptor descriptor) { + private void setSupplementalSemanticIds(List supplementalSemanticIds, SubmodelDescriptor descriptor) { if (supplementalSemanticIds == null || supplementalSemanticIds.isEmpty()) return; - descriptor.setSupplementalSemanticId(attributeMapper.mapSupplementalSemanticId(supplementalSemanticIds)); + descriptor.setSupplementalSemanticIds(attributeMapper.mapSupplementalSemanticIds(supplementalSemanticIds)); } private void setEndpointItem(String submodelId, SubmodelDescriptor descriptor, List submodelServerURLs) { diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/AttributeMapper.java b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/AttributeMapper.java index 75f3a4beb..3d2cd42ad 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/AttributeMapper.java +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/AttributeMapper.java @@ -147,15 +147,15 @@ public Reference mapSemanticId(org.eclipse.digitaltwin.aas4j.v3.model.Reference * @param supplementalSemanticIds * @return the mapped supplementalSemanticIds */ - public List mapSupplementalSemanticId(List supplementalSemanticIds) { + public List mapSupplementalSemanticIds(List supplementalSemanticIds) { CustomTypeCloneFactory cloneFactory = new CustomTypeCloneFactory<>(Reference.class, mapper); - List mappedSupplementalSemanticId = cloneFactory.create(supplementalSemanticIds); + List mappedSupplementalSemanticIds = cloneFactory.create(supplementalSemanticIds); - if (mappedSupplementalSemanticId == null) + if (mappedSupplementalSemanticIds == null) logger.error("SupplementalSemanticId could not be mapped due to a failure."); - return mappedSupplementalSemanticId; + return mappedSupplementalSemanticIds; } } diff --git a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/TestAttributeMapper.java b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/TestAttributeMapper.java index e8f31bbed..cfae58851 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/TestAttributeMapper.java +++ b/basyx.submodelregistry/basyx.submodelregistry-client-native/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/client/mapper/TestAttributeMapper.java @@ -100,10 +100,10 @@ public void mapSemanticId() { } @Test - public void mapSupplementalSemanticId() { + public void mapSupplementalSemanticIds() { List expectedSupplementalSemanticId = AttributeMapperFixture.getSubmodelRegSupplementalSemanticIds(); - List actualSupplementalSemanticId = attributeMapper.mapSupplementalSemanticId(AttributeMapperFixture.getAas4jSupplementalSemanticIds()); + List actualSupplementalSemanticId = attributeMapper.mapSupplementalSemanticIds(AttributeMapperFixture.getAas4jSupplementalSemanticIds()); assertEquals(expectedSupplementalSemanticId.size(), actualSupplementalSemanticId.size()); assertEquals(expectedSupplementalSemanticId, actualSupplementalSemanticId); diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_1.json index e9c5d704a..13ece5c9f 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_1.json +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -6,7 +6,7 @@ "idShort": "dummySubmodelIdShort_3", "id": "dummySubmodelId_3", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_2.json index 370b78932..aabfcf2f6 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_2.json +++ b/basyx.submodelregistry/basyx.submodelregistry-feature-authorization/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -6,7 +6,7 @@ "idShort": "specificSubmodelIdShort-2", "id": "specificSubmodelId-2", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java index 1fc1daa0f..dbd7d0ba4 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java @@ -304,7 +304,7 @@ public void whenSendFullObjectStructure_ItemIsProcessedProperly() throws ApiExce .symbol("$$"); EmbeddedDataSpecification edSpec = new EmbeddedDataSpecification().dataSpecification(reference).dataSpecificationContent(new DataSpecificationContent(dsContent)); AdministrativeInformation aInfo = new AdministrativeInformation().addEmbeddedDataSpecificationsItem(edSpec); - SubmodelDescriptor sm = new SubmodelDescriptor().id("sm").id("short").addDescriptionItem(dType).addDisplayNameItem(nType).addEndpointsItem(ep).addExtensionsItem(ext).addSupplementalSemanticIdItem(reference); + SubmodelDescriptor sm = new SubmodelDescriptor().id("sm").id("short").addDescriptionItem(dType).addDisplayNameItem(nType).addEndpointsItem(ep).addExtensionsItem(ext).addSupplementalSemanticIdsItem(reference); sm.setAdministration(aInfo); SubmodelDescriptor descr = api.postSubmodelDescriptor(sm); diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json index e9c5d704a..13ece5c9f 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -6,7 +6,7 @@ "idShort": "dummySubmodelIdShort_3", "id": "dummySubmodelId_3", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json index 370b78932..aabfcf2f6 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -6,7 +6,7 @@ "idShort": "specificSubmodelIdShort-2", "id": "specificSubmodelId-2", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json index e9c5d704a..13ece5c9f 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -6,7 +6,7 @@ "idShort": "dummySubmodelIdShort_3", "id": "dummySubmodelId_3", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json index 370b78932..aabfcf2f6 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-kafka-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -6,7 +6,7 @@ "idShort": "specificSubmodelIdShort-2", "id": "specificSubmodelId-2", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json index e9c5d704a..13ece5c9f 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -6,7 +6,7 @@ "idShort": "dummySubmodelIdShort_3", "id": "dummySubmodelId_3", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json index 370b78932..aabfcf2f6 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mem/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -6,7 +6,7 @@ "idShort": "specificSubmodelIdShort-2", "id": "specificSubmodelId-2", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json index e9c5d704a..13ece5c9f 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_1.json @@ -6,7 +6,7 @@ "idShort": "dummySubmodelIdShort_3", "id": "dummySubmodelId_3", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json index 370b78932..aabfcf2f6 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json +++ b/basyx.submodelregistry/basyx.submodelregistry-service-release-log-mongodb/src/test/resources/authorization/SubmodelDescriptorSimple_2.json @@ -6,7 +6,7 @@ "idShort": "specificSubmodelIdShort-2", "id": "specificSubmodelId-2", "semanticId": null, - "supplementalSemanticId": null, + "supplementalSemanticIds": null, "endpoints": [ { "interface": "AAS-3.0", diff --git a/basyx.submodelregistry/open-api/Plattform_i40-SubmodelRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml b/basyx.submodelregistry/open-api/Plattform_i40-SubmodelRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml index 756a3d9cc..9bc3ae0e0 100644 --- a/basyx.submodelregistry/open-api/Plattform_i40-SubmodelRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml +++ b/basyx.submodelregistry/open-api/Plattform_i40-SubmodelRegistryServiceSpecification-V3.0.1_SSP-001-resolved.yaml @@ -366,7 +366,7 @@ components: type: string semanticId: $ref: '#/components/schemas/Reference' - supplementalSemanticId: + supplementalSemanticIds: minItems: 1 type: array items: From ab7cfe691e807ee3d35b924ebfde015f0c195d93 Mon Sep 17 00:00:00 2001 From: FriedJannik Date: Wed, 11 Mar 2026 11:03:35 +0100 Subject: [PATCH 2/5] Adds Legacy Compatibility --- .../MongoDbSubmodelRegistryStorage.java | 27 +++++++-- .../MongoDbSubmodelRegistryStorageTest.java | 59 +++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/MongoDbSubmodelRegistryStorage.java b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/MongoDbSubmodelRegistryStorage.java index b06b6c098..c6b65bdd0 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/MongoDbSubmodelRegistryStorage.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/storage/mongodb/MongoDbSubmodelRegistryStorage.java @@ -30,6 +30,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.bson.Document; import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.eclipse.digitaltwin.basyx.submodelregistry.model.SubmodelDescriptor; @@ -58,6 +59,8 @@ public class MongoDbSubmodelRegistryStorage implements SubmodelRegistryStorage { // mongodb maps all id fields internally to _id private static final String ID = "_id"; + private static final String SUPPLEMENTAL_SEMANTIC_ID = "supplementalSemanticId"; + private static final String SUPPLEMENTAL_SEMANTIC_IDS = "supplementalSemanticIds"; private final MongoTemplate template; @@ -68,8 +71,8 @@ public CursorResult> getAllSubmodelDescriptors(@NonNull List allAggregations = new LinkedList<>(); applySorting(allAggregations); applyPagination(pRequest, allAggregations); - AggregationResults results = template.aggregate(Aggregation.newAggregation(allAggregations), collectionName, SubmodelDescriptor.class); - List foundDescriptors = results.getMappedResults(); + AggregationResults results = template.aggregate(Aggregation.newAggregation(allAggregations), collectionName, Document.class); + List foundDescriptors = results.getMappedResults().stream().map(this::toSubmodelDescriptor).collect(Collectors.toList()); String cursor = resolveCursor(pRequest, foundDescriptors); return new CursorResult>(cursor, foundDescriptors); } @@ -84,11 +87,11 @@ public Set clear() { @Override public SubmodelDescriptor getSubmodelDescriptor(@NonNull String submodelId) throws SubmodelNotFoundException { - SubmodelDescriptor descriptor = template.findById(submodelId, SubmodelDescriptor.class, collectionName); - if (descriptor == null) { + Document foundDescriptor = template.findById(submodelId, Document.class, collectionName); + if (foundDescriptor == null) { throw new SubmodelNotFoundException(submodelId); } - return descriptor; + return toSubmodelDescriptor(foundDescriptor); } @Override @@ -142,6 +145,20 @@ private void applySorting(List allAggregations) { allAggregations.add(sortOp); } + private SubmodelDescriptor toSubmodelDescriptor(Document descriptorDocument) { + Document compatibleDocument = ensureLegacyFieldCompatibility(descriptorDocument); + return template.getConverter().read(SubmodelDescriptor.class, compatibleDocument); + } + + private Document ensureLegacyFieldCompatibility(Document descriptorDocument) { + if (descriptorDocument.containsKey(SUPPLEMENTAL_SEMANTIC_IDS) || !descriptorDocument.containsKey(SUPPLEMENTAL_SEMANTIC_ID)) { + return descriptorDocument; + } + Document compatibleDocument = new Document(descriptorDocument); + compatibleDocument.put(SUPPLEMENTAL_SEMANTIC_IDS, descriptorDocument.get(SUPPLEMENTAL_SEMANTIC_ID)); + return compatibleDocument; + } + private String resolveCursor(PaginationInfo pRequest, List foundDescriptors) { if (foundDescriptors.isEmpty() || !pRequest.isPaged()) { return null; diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java index 168f6f833..b1e6d5aa8 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java @@ -26,11 +26,20 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Arrays; +import java.util.List; + import org.bson.Document; +import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; +import org.eclipse.digitaltwin.basyx.submodelregistry.model.SubmodelDescriptor; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.configuration.MongoDbConfiguration; +import org.eclipse.digitaltwin.basyx.submodelregistry.service.storage.SubmodelRegistryStorage; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.query.Query; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; @@ -47,10 +56,60 @@ public class MongoDbSubmodelRegistryStorageTest extends SubmodelRegistryStorageT @Autowired private MongoTemplate template; + @Autowired + private SubmodelRegistryStorage storage; + + @Autowired + private MongoDbConfiguration configuration; + @Test public void whenGetById_NotAllDocumentsScannedButIndexUsed() { MongoCollection collection = template.getCollection("submodeldescriptors"); Document doc = collection.find(new Document("_id", "11")).explain(ExplainVerbosity.QUERY_PLANNER); assertThat(doc.toJson()).doesNotContain("\"COLLSCAN\""); } + + @Test + public void givenLegacySupplementalSemanticId_whenGetById_thenDescriptorContainsSupplementalSemanticField() { + template.remove(new Query(), configuration.collectionName); + template.save(createLegacyDocument("legacy-get-1"), configuration.collectionName); + + SubmodelDescriptor descriptor = storage.getSubmodelDescriptor("legacy-get-1"); + Document writtenDescriptor = new Document(); + template.getConverter().write(descriptor, writtenDescriptor); + + assertThat(extractSupplementalSemanticField(writtenDescriptor)).isNotNull(); + assertThat(extractSupplementalSemanticField(writtenDescriptor)).isInstanceOf(List.class); + assertThat((List) extractSupplementalSemanticField(writtenDescriptor)).hasSize(1); + } + + @Test + public void givenLegacySupplementalSemanticId_whenGetAll_thenDescriptorContainsSupplementalSemanticField() { + template.remove(new Query(), configuration.collectionName); + template.save(createLegacyDocument("legacy-getall-1"), configuration.collectionName); + + CursorResult> result = storage.getAllSubmodelDescriptors(PaginationInfo.NO_LIMIT); + SubmodelDescriptor descriptor = result.getResult().stream().filter(x -> "legacy-getall-1".equals(x.getId())).findFirst().orElse(null); + assertThat(descriptor).isNotNull(); + + Document writtenDescriptor = new Document(); + template.getConverter().write(descriptor, writtenDescriptor); + + assertThat(extractSupplementalSemanticField(writtenDescriptor)).isNotNull(); + assertThat(extractSupplementalSemanticField(writtenDescriptor)).isInstanceOf(List.class); + assertThat((List) extractSupplementalSemanticField(writtenDescriptor)).hasSize(1); + } + + private Object extractSupplementalSemanticField(Document descriptorDocument) { + if (descriptorDocument.containsKey("supplementalSemanticIds")) { + return descriptorDocument.get("supplementalSemanticIds"); + } + return descriptorDocument.get("supplementalSemanticId"); + } + + private Document createLegacyDocument(String id) { + Document key = new Document("type", "GlobalReference").append("value", "urn:test:" + id); + Document reference = new Document("type", "ExternalReference").append("keys", Arrays.asList(key)); + return new Document("_id", id).append("idShort", "short-" + id).append("endpoints", Arrays.asList()).append("supplementalSemanticId", Arrays.asList(reference)); + } } \ No newline at end of file From 7efd1575cb8ebdf744a116f7b172d18b4481cea2 Mon Sep 17 00:00:00 2001 From: Aaron Zielstorff Date: Wed, 11 Mar 2026 14:59:09 +0100 Subject: [PATCH 3/5] Fixes test failures and adds legacy support to aas registry --- .../mongodb/MongoDbAasRegistryStorage.java | 77 +++++++++++++++---- .../tests/MongoDbAasRegistryStorageTest.java | 56 +++++++++++++- .../MongoDbSubmodelRegistryStorageTest.java | 4 +- 3 files changed, 121 insertions(+), 16 deletions(-) diff --git a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/MongoDbAasRegistryStorage.java b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/MongoDbAasRegistryStorage.java index 389291be3..9010fddf3 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/MongoDbAasRegistryStorage.java +++ b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/main/java/org/eclipse/digitaltwin/basyx/aasregistry/service/storage/mongodb/MongoDbAasRegistryStorage.java @@ -36,6 +36,7 @@ import java.util.stream.Collectors; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.bson.Document; import org.eclipse.digitaltwin.basyx.aasregistry.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.model.AssetKind; import org.eclipse.digitaltwin.basyx.aasregistry.model.ShellDescriptorQuery; @@ -80,6 +81,8 @@ public class MongoDbAasRegistryStorage implements AasRegistryStorage { private static final String SUBMODEL_DESCRIPTORS_ID = "submodelDescriptors._id"; private static final String ASSET_TYPE = "assetType"; private static final String ASSET_KIND = "assetKind"; + private static final String SUPPLEMENTAL_SEMANTIC_ID = "supplementalSemanticId"; + private static final String SUPPLEMENTAL_SEMANTIC_IDS = "supplementalSemanticIds"; private final MongoTemplate template; @@ -91,8 +94,8 @@ public CursorResult> getAllAasDescripto applyFilter(filter, allAggregations); applySorting(allAggregations); applyPagination(pRequest, allAggregations); - AggregationResults results = template.aggregate(Aggregation.newAggregation(allAggregations), collectionName, AssetAdministrationShellDescriptor.class); - List foundDescriptors = results.getMappedResults(); + AggregationResults results = template.aggregate(Aggregation.newAggregation(allAggregations), collectionName, Document.class); + List foundDescriptors = results.getMappedResults().stream().map(this::toAasDescriptor).collect(Collectors.toList()); String cursor = resolveCursor(pRequest, foundDescriptors, AssetAdministrationShellDescriptor::getId); return new CursorResult<>(cursor, foundDescriptors); } @@ -148,11 +151,11 @@ public Optional createFilterCriteria(DescriptorFilter filter) { @Override public AssetAdministrationShellDescriptor getAasDescriptor(@NonNull String aasDescriptorId) throws AasDescriptorNotFoundException { - AssetAdministrationShellDescriptor descriptor = template.findById(aasDescriptorId, AssetAdministrationShellDescriptor.class, collectionName); + Document descriptor = template.findById(aasDescriptorId, Document.class, collectionName); if (descriptor == null) { throw new AasDescriptorNotFoundException(aasDescriptorId); } - return descriptor; + return toAasDescriptor(descriptor); } @Override @@ -212,8 +215,8 @@ public CursorResult> getAllSubmodels(@NonNull String aa allAggregations.add(Aggregation.replaceRoot(SUBMODEL_DESCRIPTORS)); this.applySorting(allAggregations); this.applyPagination(pRequest, allAggregations); - AggregationResults results = template.aggregate(Aggregation.newAggregation(allAggregations), collectionName, SubmodelDescriptor.class); - List submodels = results.getMappedResults(); + AggregationResults results = template.aggregate(Aggregation.newAggregation(allAggregations), collectionName, Document.class); + List submodels = results.getMappedResults().stream().map(this::toSubmodelDescriptor).collect(Collectors.toList()); String cursor = resolveCursor(pRequest, submodels, SubmodelDescriptor::getId); return new CursorResult<>(cursor, submodels); } @@ -224,16 +227,21 @@ public SubmodelDescriptor getSubmodel(@NonNull String aasDescriptorId, @NonNull all.add(Aggregation.match(Criteria.where(ID).is(aasDescriptorId))); ArrayOperators.Filter filter = ArrayOperators.arrayOf(SUBMODEL_DESCRIPTORS).filter().as(SUBMODEL_DESCRIPTORS).by(ComparisonOperators.valueOf(SUBMODEL_DESCRIPTORS_ID).equalToValue(submodelId)); all.add(Aggregation.project().and(filter).as(SUBMODEL_DESCRIPTORS)); - AggregationResults results = template.aggregate(Aggregation.newAggregation(all), collectionName, AssetAdministrationShellDescriptor.class); - List aasDescriptors = results.getMappedResults(); + AggregationResults results = template.aggregate(Aggregation.newAggregation(all), collectionName, Document.class); + List aasDescriptors = results.getMappedResults(); if (aasDescriptors.isEmpty()) { throw new AasDescriptorNotFoundException(aasDescriptorId); } - List descriptors = aasDescriptors.get(0).getSubmodelDescriptors(); - if (descriptors == null || descriptors.isEmpty()) { + Document compatibleAasDescriptor = ensureLegacyAasDescriptorCompatibility(aasDescriptors.get(0)); + Object descriptorsObject = compatibleAasDescriptor.get(SUBMODEL_DESCRIPTORS); + if (!(descriptorsObject instanceof List descriptors) || descriptors.isEmpty()) { throw new SubmodelNotFoundException(aasDescriptorId, submodelId); } - return descriptors.get(0); + Object firstDescriptor = descriptors.get(0); + if (!(firstDescriptor instanceof Document descriptorDocument)) { + throw new SubmodelNotFoundException(aasDescriptorId, submodelId); + } + return toSubmodelDescriptor(descriptorDocument); } @Override @@ -321,9 +329,52 @@ public ShellDescriptorSearchResponse searchAasDescriptors(@NonNull ShellDescript qBuilder.withProjection(grouped.getQueriesInsideSubmodel(), aggregationOps); Aggregation aggregation = Aggregation.newAggregation(aggregationOps); - AggregationResults results = template.aggregate(aggregation, collectionName, AssetAdministrationShellDescriptor.class); + AggregationResults results = template.aggregate(aggregation, collectionName, Document.class); - List descriptors = results.getMappedResults(); + List descriptors = results.getMappedResults().stream().map(this::toAasDescriptor).collect(Collectors.toList()); return new ShellDescriptorSearchResponse(total, descriptors); } + + private AssetAdministrationShellDescriptor toAasDescriptor(Document descriptorDocument) { + Document compatibleDocument = ensureLegacyAasDescriptorCompatibility(descriptorDocument); + return template.getConverter().read(AssetAdministrationShellDescriptor.class, compatibleDocument); + } + + private SubmodelDescriptor toSubmodelDescriptor(Document descriptorDocument) { + Document compatibleDocument = ensureLegacySubmodelDescriptorCompatibility(descriptorDocument); + return template.getConverter().read(SubmodelDescriptor.class, compatibleDocument); + } + + private Document ensureLegacyAasDescriptorCompatibility(Document descriptorDocument) { + Object submodelDescriptorsObject = descriptorDocument.get(SUBMODEL_DESCRIPTORS); + if (!(submodelDescriptorsObject instanceof List submodelDescriptors)) { + return descriptorDocument; + } + boolean changed = false; + List compatibleSubmodels = new ArrayList<>(submodelDescriptors.size()); + for (Object eachSubmodel : submodelDescriptors) { + if (eachSubmodel instanceof Document submodelDocument) { + Document compatibleSubmodel = ensureLegacySubmodelDescriptorCompatibility(submodelDocument); + compatibleSubmodels.add(compatibleSubmodel); + changed |= compatibleSubmodel != submodelDocument; + } else { + compatibleSubmodels.add(eachSubmodel); + } + } + if (!changed) { + return descriptorDocument; + } + Document compatibleDescriptor = new Document(descriptorDocument); + compatibleDescriptor.put(SUBMODEL_DESCRIPTORS, compatibleSubmodels); + return compatibleDescriptor; + } + + private Document ensureLegacySubmodelDescriptorCompatibility(Document descriptorDocument) { + if (descriptorDocument.containsKey(SUPPLEMENTAL_SEMANTIC_IDS) || !descriptorDocument.containsKey(SUPPLEMENTAL_SEMANTIC_ID)) { + return descriptorDocument; + } + Document compatibleDocument = new Document(descriptorDocument); + compatibleDocument.put(SUPPLEMENTAL_SEMANTIC_IDS, descriptorDocument.get(SUPPLEMENTAL_SEMANTIC_ID)); + return compatibleDocument; + } } diff --git a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/MongoDbAasRegistryStorageTest.java b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/MongoDbAasRegistryStorageTest.java index e3a4bf5c3..b42cccfe7 100644 --- a/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/MongoDbAasRegistryStorageTest.java +++ b/basyx.aasregistry/basyx.aasregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/aasregistry/service/tests/MongoDbAasRegistryStorageTest.java @@ -26,18 +26,24 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.Arrays; +import java.util.List; import java.util.Optional; import org.bson.Document; +import org.eclipse.digitaltwin.basyx.aasregistry.model.AssetAdministrationShellDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.model.AssetKind; import org.eclipse.digitaltwin.basyx.aasregistry.model.ShellDescriptorQuery; import org.eclipse.digitaltwin.basyx.aasregistry.model.ShellDescriptorQuery.QueryTypeEnum; +import org.eclipse.digitaltwin.basyx.aasregistry.model.SubmodelDescriptor; import org.eclipse.digitaltwin.basyx.aasregistry.paths.AasRegistryPaths; import org.eclipse.digitaltwin.basyx.aasregistry.service.configuration.MongoDbConfiguration; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.DescriptorFilter; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.ShellDescriptorSearchRequests; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.mongodb.MongoDbAasRegistryStorage; import org.eclipse.digitaltwin.basyx.aasregistry.service.storage.mongodb.SearchQueryBuilder; +import org.eclipse.digitaltwin.basyx.core.pagination.CursorResult; +import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -51,7 +57,7 @@ import com.mongodb.client.MongoCollection; @TestPropertySource(properties = { "registry.type=mongodb", "spring.data.mongodb.database=aasregistry" - , "spring.data.mongodb.uri=mongodb://mongoAdmin:mongoPassword@localhost:27017" }) + , "spring.data.mongodb.uri=mongodb://mongoAdmin:mongoPassword@localhost:27017", "basyx.aasregistry.mongodb.collectionName=aasdescriptors" }) @ContextConfiguration(classes = { MongoDbConfiguration.class }) @EnableAutoConfiguration public class MongoDbAasRegistryStorageTest extends AasRegistryStorageTest { @@ -111,6 +117,39 @@ public void whenGetByAasID_NotAllDocumentsScannedButIndexUsed() { assertThat(doc.toJson()).doesNotContain("\"COLLSCAN\""); } + @Test + public void givenLegacySupplementalSemanticIdInSubmodel_whenGetAasById_thenSubmodelContainsSupplementalSemanticField() { + template.remove(new Query(), "aasdescriptors"); + template.save(createLegacyAasDocument("legacy-aas-1", "legacy-submodel-1"), "aasdescriptors"); + + AssetAdministrationShellDescriptor descriptor = storage.getAasDescriptor("legacy-aas-1"); + SubmodelDescriptor submodelDescriptor = descriptor.getSubmodelDescriptors().get(0); + + Document writtenSubmodelDescriptor = new Document(); + template.getConverter().write(submodelDescriptor, writtenSubmodelDescriptor); + + assertThat(extractSupplementalSemanticField(writtenSubmodelDescriptor)).isNotNull(); + assertThat(extractSupplementalSemanticField(writtenSubmodelDescriptor)).isInstanceOf(List.class); + assertThat((List) extractSupplementalSemanticField(writtenSubmodelDescriptor)).hasSize(1); + } + + @Test + public void givenLegacySupplementalSemanticIdInSubmodel_whenGetAllSubmodels_thenSubmodelContainsSupplementalSemanticField() { + template.remove(new Query(), "aasdescriptors"); + template.save(createLegacyAasDocument("legacy-aas-2", "legacy-submodel-2"), "aasdescriptors"); + + CursorResult> result = storage.getAllSubmodels("legacy-aas-2", PaginationInfo.NO_LIMIT); + SubmodelDescriptor submodelDescriptor = result.getResult().stream().filter(x -> "legacy-submodel-2".equals(x.getId())).findFirst().orElse(null); + assertThat(submodelDescriptor).isNotNull(); + + Document writtenSubmodelDescriptor = new Document(); + template.getConverter().write(submodelDescriptor, writtenSubmodelDescriptor); + + assertThat(extractSupplementalSemanticField(writtenSubmodelDescriptor)).isNotNull(); + assertThat(extractSupplementalSemanticField(writtenSubmodelDescriptor)).isInstanceOf(List.class); + assertThat((List) extractSupplementalSemanticField(writtenSubmodelDescriptor)).hasSize(1); + } + private void testIndexFilter(AssetKind kind, String type) { MongoDbAasRegistryStorage storage = new MongoDbAasRegistryStorage(template, "aasdescriptors"); Optional criteriaOpt = storage.createFilterCriteria(new DescriptorFilter(kind, type)); @@ -124,4 +163,19 @@ private void testIndexFilter(Criteria criteria) { Document doc = collection.find(Query.query(criteria).getQueryObject()).explain(ExplainVerbosity.QUERY_PLANNER); assertThat(doc.toJson()).doesNotContain("\"COLLSCAN\""); } + + private Object extractSupplementalSemanticField(Document descriptorDocument) { + if (descriptorDocument.containsKey("supplementalSemanticIds")) { + return descriptorDocument.get("supplementalSemanticIds"); + } + return descriptorDocument.get("supplementalSemanticId"); + } + + private Document createLegacyAasDocument(String aasId, String submodelId) { + Document key = new Document("type", "FILE").append("value", "urn:test:" + submodelId); + Document reference = new Document("type", "EXTERNALREFERENCE").append("keys", Arrays.asList(key)); + Document submodelDescriptor = new Document("_id", submodelId).append("idShort", "short-" + submodelId).append("endpoints", Arrays.asList()) + .append("supplementalSemanticId", Arrays.asList(reference)); + return new Document("_id", aasId).append("idShort", "short-" + aasId).append("endpoints", Arrays.asList()).append("submodelDescriptors", Arrays.asList(submodelDescriptor)); + } } \ No newline at end of file diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java index b1e6d5aa8..393a388de 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-mongodb-storage/src/test/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/MongoDbSubmodelRegistryStorageTest.java @@ -108,8 +108,8 @@ private Object extractSupplementalSemanticField(Document descriptorDocument) { } private Document createLegacyDocument(String id) { - Document key = new Document("type", "GlobalReference").append("value", "urn:test:" + id); - Document reference = new Document("type", "ExternalReference").append("keys", Arrays.asList(key)); + Document key = new Document("type", "FILE").append("value", "urn:test:" + id); + Document reference = new Document("type", "EXTERNALREFERENCE").append("keys", Arrays.asList(key)); return new Document("_id", id).append("idShort", "short-" + id).append("endpoints", Arrays.asList()).append("supplementalSemanticId", Arrays.asList(reference)); } } \ No newline at end of file From 9082187e1f6e7ed48c0a24d142e2abc419cc2547 Mon Sep 17 00:00:00 2001 From: Aaron Zielstorff Date: Thu, 23 Apr 2026 19:52:44 +0200 Subject: [PATCH 4/5] fix(security): mitigate blind SSRF in operation delegation (CWE-918) Add strict delegation target validation with secure defaults and explicit allowlist support. Block local/private/link-local/metadata destinations, reject redirects, and extend tests/docs including example config updates. --- .../Readme.md | 27 ++ .../delegation/HTTPOperationDelegation.java | 12 +- ...OperationDelegationSecurityProperties.java | 132 +++++++ ...gationSubmodelRepositoryConfiguration.java | 24 +- .../OperationDelegationTargetValidator.java | 326 ++++++++++++++++++ .../operation/delegation/HTTPMockServer.java | 6 + .../TestOperationDelegationFeature.java | 107 +++++- ...estOperationDelegationTargetValidator.java | 96 ++++++ .../src/main/resources/application.properties | 9 + examples/BaSyxOperationDelegation/README.md | 20 +- .../basyx/aas-env.properties | 2 + .../docker-compose.yml | 6 +- 12 files changed, 748 insertions(+), 19 deletions(-) create mode 100644 basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSecurityProperties.java create mode 100644 basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationTargetValidator.java create mode 100644 basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationTargetValidator.java diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/Readme.md b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/Readme.md index 4e47744fe..c69f6d0a8 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/Readme.md +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/Readme.md @@ -30,3 +30,30 @@ This means that operations can be delegated to endpoints of the same server as w The independent destination where the request is to be delegated should support the request with a parameter ([OperationVariable[]](https://github.com/eclipse-aas4j/aas4j/blob/2abf04bc01f80bceafa575cf85da429d5fe63918/model/src/main/java/org/eclipse/digitaltwin/aas4j/v3/model/OperationVariable.java#L31)) and provide the output in a strict format ([OperationVariable[]](https://github.com/eclipse-aas4j/aas4j/blob/2abf04bc01f80bceafa575cf85da429d5fe63918/model/src/main/java/org/eclipse/digitaltwin/aas4j/v3/model/OperationVariable.java#L31)) As of now, only delegation to HTTP URLs is supported. + +## Security defaults for operation delegation + +Operation delegation now validates each outbound delegation URI before dispatch. + +- Only `http` and `https` URIs are allowed. +- Loopback, private, link-local, and metadata addresses are blocked by default. +- Redirect responses are rejected. +- Explicit allowlists can be configured for approved hybrid deployments. + +Example configuration: + +``` +basyx.submodelrepository.feature.operation.delegation.security.enabled = true +basyx.submodelrepository.feature.operation.delegation.security.denyPrivateTargets = true +basyx.submodelrepository.feature.operation.delegation.security.denyLinkLocalTargets = true +basyx.submodelrepository.feature.operation.delegation.security.denyLoopbackTargets = true +basyx.submodelrepository.feature.operation.delegation.security.denyMetadataTargets = true +basyx.submodelrepository.feature.operation.delegation.security.denyRedirects = true + +# Optional explicit overrides for trusted targets +basyx.submodelrepository.feature.operation.delegation.security.allowlist.hosts = localhost,*.trusted.example +basyx.submodelrepository.feature.operation.delegation.security.allowlist.cidrs = 10.42.0.0/16,127.0.0.0/8,::1/128 +basyx.submodelrepository.feature.operation.delegation.security.allowlist.ports = 443,8443 +``` + +If an existing setup delegates to localhost or private network endpoints, configure the corresponding allowlist entries explicitly. diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPOperationDelegation.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPOperationDelegation.java index d9968f920..3f878276a 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPOperationDelegation.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPOperationDelegation.java @@ -42,7 +42,7 @@ public class HTTPOperationDelegation implements OperationDelegation { public static final String INVOCATION_DELEGATION_TYPE = "invocationDelegation"; - private WebClient webClient; + private final WebClient webClient; public HTTPOperationDelegation(WebClient webClient) { this.webClient = webClient; @@ -55,6 +55,10 @@ public OperationVariable[] delegate(Qualifier qualifier, OperationVariable[] inp try { return webClient.post().uri(uri).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(input)).exchangeToMono(response -> { + if (response.statusCode().is3xxRedirection()) { + throw new OperationDelegationException(String.format("Unable to delegate the invocation operation on the URI: '%s' redirects are not allowed", uri)); + } + if (response.statusCode().isError()) { throw new OperationDelegationException(String.format("Unable to delegate the invocation operation on the URI: '%s' the response code is %s", uri, response.statusCode())); } else { @@ -62,8 +66,12 @@ public OperationVariable[] delegate(Qualifier qualifier, OperationVariable[] inp } }).block(); + } catch (OperationDelegationException e) { + throw e; } catch (WebClientResponseException e) { - throw new OperationDelegationException(String.format("Exception occurred while invocing operation on the URI: '%s' the error is %s", uri, e.getStackTrace())); + throw new OperationDelegationException(String.format("Exception occurred while invoking operation on the URI: '%s' the error is %s", uri, e.getMessage())); + } catch (Exception e) { + throw new OperationDelegationException(String.format("Exception occurred while invoking operation on the URI: '%s' the error is %s", uri, e.getMessage())); } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSecurityProperties.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSecurityProperties.java new file mode 100644 index 000000000..91f566886 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSecurityProperties.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (C) 2026 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelrepository.feature.operation.delegation; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Security properties for outbound operation delegation requests. + */ +@ConfigurationProperties(prefix = OperationDelegationSubmodelRepositoryFeature.FEATURENAME + ".security") +public class OperationDelegationSecurityProperties { + + private boolean enabled = true; + private boolean denyPrivateTargets = true; + private boolean denyLinkLocalTargets = true; + private boolean denyLoopbackTargets = true; + private boolean denyMetadataTargets = true; + private boolean denyRedirects = true; + private Allowlist allowlist = new Allowlist(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isDenyPrivateTargets() { + return denyPrivateTargets; + } + + public void setDenyPrivateTargets(boolean denyPrivateTargets) { + this.denyPrivateTargets = denyPrivateTargets; + } + + public boolean isDenyLinkLocalTargets() { + return denyLinkLocalTargets; + } + + public void setDenyLinkLocalTargets(boolean denyLinkLocalTargets) { + this.denyLinkLocalTargets = denyLinkLocalTargets; + } + + public boolean isDenyLoopbackTargets() { + return denyLoopbackTargets; + } + + public void setDenyLoopbackTargets(boolean denyLoopbackTargets) { + this.denyLoopbackTargets = denyLoopbackTargets; + } + + public boolean isDenyMetadataTargets() { + return denyMetadataTargets; + } + + public void setDenyMetadataTargets(boolean denyMetadataTargets) { + this.denyMetadataTargets = denyMetadataTargets; + } + + public boolean isDenyRedirects() { + return denyRedirects; + } + + public void setDenyRedirects(boolean denyRedirects) { + this.denyRedirects = denyRedirects; + } + + public Allowlist getAllowlist() { + return allowlist; + } + + public void setAllowlist(Allowlist allowlist) { + this.allowlist = allowlist; + } + + public static class Allowlist { + private List hosts = new ArrayList<>(); + private List cidrs = new ArrayList<>(); + private List ports = new ArrayList<>(); + + public List getHosts() { + return hosts; + } + + public void setHosts(List hosts) { + this.hosts = hosts; + } + + public List getCidrs() { + return cidrs; + } + + public void setCidrs(List cidrs) { + this.cidrs = cidrs; + } + + public List getPorts() { + return ports; + } + + public void setPorts(List ports) { + this.ports = ports; + } + } +} diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepositoryConfiguration.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepositoryConfiguration.java index 785c03efc..d04770c1f 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepositoryConfiguration.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepositoryConfiguration.java @@ -28,10 +28,12 @@ import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; @@ -44,22 +46,36 @@ */ @Configuration @ConditionalOnExpression("${" + OperationDelegationSubmodelRepositoryFeature.FEATURENAME + ".enabled:true}") +@EnableConfigurationProperties(OperationDelegationSecurityProperties.class) public class OperationDelegationSubmodelRepositoryConfiguration { @Bean @ConditionalOnMissingBean - public OperationDelegation getOperationDelegation(ObjectMapper mapper) { + public OperationDelegationTargetValidator operationDelegationTargetValidator(OperationDelegationSecurityProperties securityProperties) { + return new OperationDelegationTargetValidator(securityProperties); + } + + @Bean + @ConditionalOnMissingBean + public OperationDelegation getOperationDelegation(ObjectMapper mapper, OperationDelegationTargetValidator targetValidator) { - return new HTTPOperationDelegation(createWebClient(mapper)); + return new HTTPOperationDelegation(createWebClient(mapper, targetValidator)); } - private WebClient createWebClient(ObjectMapper mapper) { + private WebClient createWebClient(ObjectMapper mapper, OperationDelegationTargetValidator targetValidator) { ExchangeStrategies strategies = ExchangeStrategies.builder().codecs(configurer -> { configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper)); configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper)); }).build(); - return WebClient.builder().exchangeStrategies(strategies).build(); + return WebClient.builder().exchangeStrategies(strategies).filter(validateTargetFilter(targetValidator)).build(); + } + + private ExchangeFilterFunction validateTargetFilter(OperationDelegationTargetValidator targetValidator) { + return (request, next) -> { + targetValidator.validate(request.url()); + return next.exchange(request); + }; } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationTargetValidator.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationTargetValidator.java new file mode 100644 index 000000000..3c445cd2f --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationTargetValidator.java @@ -0,0 +1,326 @@ +/******************************************************************************* + * Copyright (C) 2026 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelrepository.feature.operation.delegation; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import org.eclipse.digitaltwin.basyx.core.exceptions.OperationDelegationException; + +/** + * Validates operation delegation targets before outbound HTTP dispatch. + */ +public class OperationDelegationTargetValidator { + + private static final String HTTP = "http"; + private static final String HTTPS = "https"; + + private final OperationDelegationSecurityProperties securityProperties; + private final List allowlistedCidrs; + private final List allowlistedHosts; + + public OperationDelegationTargetValidator(OperationDelegationSecurityProperties securityProperties) { + this.securityProperties = Objects.requireNonNull(securityProperties, "securityProperties must not be null"); + this.allowlistedHosts = sanitizeHostPatterns(securityProperties.getAllowlist().getHosts()); + this.allowlistedCidrs = parseAllowlistedCidrs(securityProperties.getAllowlist().getCidrs()); + } + + public void validate(URI targetUri) { + if (!securityProperties.isEnabled()) + return; + + if (targetUri == null) + throw new OperationDelegationException("Delegation URI must not be null"); + + String scheme = normalize(targetUri.getScheme()); + if (!HTTP.equals(scheme) && !HTTPS.equals(scheme)) { + throw new OperationDelegationException(String.format("Delegation URI scheme '%s' is not supported", targetUri.getScheme())); + } + + String host = normalize(targetUri.getHost()); + if (host.isEmpty()) + throw new OperationDelegationException("Delegation URI must contain a host"); + + int targetPort = resolveTargetPort(targetUri, scheme); + validatePort(targetPort, targetUri); + + boolean hostAllowlisted = isHostAllowlisted(host); + InetAddress[] resolvedAddresses = resolveHost(host, targetUri); + + for (InetAddress resolvedAddress : resolvedAddresses) { + if (hostAllowlisted || isAddressAllowlisted(resolvedAddress)) + continue; + + if (isBlocked(resolvedAddress)) { + throw new OperationDelegationException(String.format("Delegation target '%s' resolves to blocked address '%s'", targetUri, resolvedAddress.getHostAddress())); + } + } + } + + private int resolveTargetPort(URI targetUri, String scheme) { + if (targetUri.getPort() > 0) + return targetUri.getPort(); + + return HTTPS.equals(scheme) ? 443 : 80; + } + + private void validatePort(int targetPort, URI targetUri) { + List allowlistedPorts = securityProperties.getAllowlist().getPorts(); + if (allowlistedPorts == null || allowlistedPorts.isEmpty()) + return; + + if (!allowlistedPorts.contains(targetPort)) { + throw new OperationDelegationException(String.format("Delegation target '%s' uses blocked port '%d'", targetUri, targetPort)); + } + } + + private InetAddress[] resolveHost(String host, URI targetUri) { + try { + InetAddress[] resolvedAddresses = InetAddress.getAllByName(host); + if (resolvedAddresses.length == 0) + throw new OperationDelegationException(String.format("Delegation target '%s' could not be resolved", targetUri)); + + return resolvedAddresses; + } catch (UnknownHostException e) { + throw new OperationDelegationException(String.format("Delegation target '%s' could not be resolved", targetUri)); + } + } + + private boolean isHostAllowlisted(String host) { + if (allowlistedHosts.isEmpty()) + return false; + + for (String hostPattern : allowlistedHosts) { + if (hostPattern.startsWith("*.")) { + String suffix = hostPattern.substring(1); + if (host.endsWith(suffix) && host.length() > suffix.length()) + return true; + continue; + } + + if (hostPattern.startsWith(".")) { + if (host.endsWith(hostPattern) && host.length() > hostPattern.length()) + return true; + continue; + } + + if (host.equals(hostPattern)) + return true; + } + + return false; + } + + private boolean isAddressAllowlisted(InetAddress address) { + for (CidrBlock allowlistedCidr : allowlistedCidrs) { + if (allowlistedCidr.matches(address)) + return true; + } + + return false; + } + + private boolean isBlocked(InetAddress address) { + if (address.isAnyLocalAddress() || address.isMulticastAddress()) + return true; + + if (securityProperties.isDenyLoopbackTargets() && address.isLoopbackAddress()) + return true; + + if (securityProperties.isDenyLinkLocalTargets() && address.isLinkLocalAddress()) + return true; + + if (securityProperties.isDenyPrivateTargets() && isPrivateAddress(address)) + return true; + + if (securityProperties.isDenyMetadataTargets() && isMetadataAddress(address)) + return true; + + return false; + } + + private boolean isPrivateAddress(InetAddress address) { + if (address instanceof Inet4Address) { + byte[] bytes = address.getAddress(); + int b0 = Byte.toUnsignedInt(bytes[0]); + int b1 = Byte.toUnsignedInt(bytes[1]); + + if (b0 == 10) + return true; + + if (b0 == 172 && b1 >= 16 && b1 <= 31) + return true; + + if (b0 == 192 && b1 == 168) + return true; + + return address.isSiteLocalAddress(); + } + + if (address instanceof Inet6Address) { + byte[] bytes = address.getAddress(); + return isUniqueLocalIpv6(bytes) || address.isSiteLocalAddress(); + } + + return false; + } + + private boolean isMetadataAddress(InetAddress address) { + if (!(address instanceof Inet4Address)) + return false; + + byte[] bytes = address.getAddress(); + return Byte.toUnsignedInt(bytes[0]) == 169 && Byte.toUnsignedInt(bytes[1]) == 254 && Byte.toUnsignedInt(bytes[2]) == 169 && Byte.toUnsignedInt(bytes[3]) == 254; + } + + private boolean isUniqueLocalIpv6(byte[] bytes) { + return bytes.length == 16 && (bytes[0] & (byte) 0xFE) == (byte) 0xFC; + } + + private List sanitizeHostPatterns(List hostPatterns) { + if (hostPatterns == null || hostPatterns.isEmpty()) + return Collections.emptyList(); + + List sanitizedPatterns = new ArrayList<>(); + for (String hostPattern : hostPatterns) { + String normalizedPattern = normalize(hostPattern); + if (!normalizedPattern.isEmpty()) + sanitizedPatterns.add(normalizedPattern); + } + + return sanitizedPatterns; + } + + private List parseAllowlistedCidrs(List cidrs) { + if (cidrs == null || cidrs.isEmpty()) + return Collections.emptyList(); + + List parsedCidrs = new ArrayList<>(); + for (String cidr : cidrs) { + String normalizedCidr = normalize(cidr); + if (normalizedCidr.isEmpty()) + continue; + + parsedCidrs.add(CidrBlock.parse(normalizedCidr)); + } + + return parsedCidrs; + } + + private String normalize(String value) { + if (value == null) + return ""; + + String normalized = value.trim().toLowerCase(Locale.ROOT); + if (normalized.endsWith(".")) + return normalized.substring(0, normalized.length() - 1); + + return normalized; + } + + private static class CidrBlock { + private final byte[] network; + private final int prefixLength; + private final int totalBits; + + private CidrBlock(byte[] network, int prefixLength) { + this.network = network; + this.prefixLength = prefixLength; + this.totalBits = network.length * 8; + } + + static CidrBlock parse(String cidr) { + String[] parts = cidr.split("/"); + if (parts.length != 2) + throw new IllegalArgumentException(String.format("Invalid CIDR allowlist entry '%s'", cidr)); + + InetAddress baseAddress; + int prefixLength; + try { + baseAddress = InetAddress.getByName(parts[0]); + prefixLength = Integer.parseInt(parts[1]); + } catch (Exception e) { + throw new IllegalArgumentException(String.format("Invalid CIDR allowlist entry '%s'", cidr)); + } + + byte[] addressBytes = baseAddress.getAddress(); + int totalBits = addressBytes.length * 8; + if (prefixLength < 0 || prefixLength > totalBits) + throw new IllegalArgumentException(String.format("Invalid CIDR prefix in allowlist entry '%s'", cidr)); + + return new CidrBlock(applyMask(addressBytes, prefixLength), prefixLength); + } + + boolean matches(InetAddress address) { + byte[] addressBytes = address.getAddress(); + if (addressBytes.length * 8 != totalBits) + return false; + + byte[] maskedCandidate = applyMask(addressBytes, prefixLength); + if (maskedCandidate.length != network.length) + return false; + + for (int i = 0; i < network.length; i++) { + if (network[i] != maskedCandidate[i]) + return false; + } + + return true; + } + + private static byte[] applyMask(byte[] input, int prefixLength) { + byte[] result = new byte[input.length]; + int remainingBits = prefixLength; + + for (int i = 0; i < input.length; i++) { + if (remainingBits >= 8) { + result[i] = input[i]; + remainingBits -= 8; + continue; + } + + if (remainingBits <= 0) { + result[i] = 0; + continue; + } + + int mask = (0xFF << (8 - remainingBits)) & 0xFF; + result[i] = (byte) (input[i] & mask); + remainingBits = 0; + } + + return result; + } + } +} diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPMockServer.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPMockServer.java index 745584273..0ab476133 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPMockServer.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/HTTPMockServer.java @@ -88,4 +88,10 @@ public void createExpectationsForPostRequest(String path, String requestBody, St .respond(HttpResponse.response().withStatusCode(expectedResponseCode.code()).withBody(expectedResponse).withContentType(MediaType.APPLICATION_JSON)); } + public void createExpectationsForPostRequest(String path, String requestBody, String expectedResponse, int expectedResponseCode) throws JsonMappingException, JsonProcessingException { + + clientAndServer.when(HttpRequest.request().withMethod("POST").withPath(path).withBody(requestBody)) + .respond(HttpResponse.response().withStatusCode(expectedResponseCode).withBody(expectedResponse).withContentType(MediaType.APPLICATION_JSON)); + } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java index e32159d7f..e08377937 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationFeature.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.sun.net.httpserver.HttpServer; import org.eclipse.digitaltwin.aas4j.v3.model.*; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultOperationVariable; import org.eclipse.digitaltwin.aas4j.v3.model.impl.DefaultProperty; @@ -56,10 +57,15 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; /** * Tests the {@link OperationDelegationSubmodelRepository} feature @@ -77,7 +83,7 @@ public class TestOperationDelegationFeature { public static void setUp() { httpMockServer.start(); - webClient = createWebClient(); + webClient = createWebClient(createSecurityPropertiesWithLocalhostAllowlist()); submodelRepository = createOperationDelegationSubmodelRepository(new HTTPOperationDelegation(webClient)); } @@ -135,6 +141,73 @@ public void invokeFailOperationDelegation() throws FileNotFoundException, IOExce submodelRepository.invokeOperation(submodelId, "operationDelegationSME", inputOperationVariable); } + + @Test + public void invokeOperationDelegationBlocksLoopbackByDefault() throws Exception { + AtomicBoolean internalServerHit = new AtomicBoolean(false); + HttpServer internalServer = HttpServer.create(new InetSocketAddress("127.0.0.1", 9090), 0); + internalServer.createContext("/internal-plc-api", exchange -> { + internalServerHit.set(true); + byte[] response = "[]".getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + exchange.getResponseBody().write(response); + exchange.close(); + }); + internalServer.start(); + + try { + OperationDelegationSecurityProperties strictSecurityProperties = new OperationDelegationSecurityProperties(); + WebClient strictWebClient = createWebClient(strictSecurityProperties); + SubmodelRepository strictRepository = createOperationDelegationSubmodelRepository(new HTTPOperationDelegation(strictWebClient)); + + String submodelId = "blockedLoopbackDelegationSubmodel"; + createSubmodelAtRepository(strictRepository, submodelId); + createInvokableSMEAtRepository(strictRepository, submodelId, "operationDelegationSME", "http://127.0.0.1:9090/internal-plc-api"); + + strictRepository.invokeOperation(submodelId, "operationDelegationSME", getInputVariable()); + fail("Expected OperationDelegationException for blocked loopback delegation target"); + } catch (OperationDelegationException e) { + assertFalse("Blocked loopback request must not reach internal server", internalServerHit.get()); + } finally { + internalServer.stop(0); + } + } + + @Test + public void invokeOperationDelegationAllowsLoopbackWhenExplicitlyAllowlisted() throws FileNotFoundException, IOException { + OperationDelegationSecurityProperties allowlistedSecurityProperties = new OperationDelegationSecurityProperties(); + allowlistedSecurityProperties.getAllowlist().setCidrs(Arrays.asList("127.0.0.0/8")); + allowlistedSecurityProperties.getAllowlist().setPorts(Arrays.asList(2020)); + + WebClient allowlistedWebClient = createWebClient(allowlistedSecurityProperties); + SubmodelRepository allowlistedRepository = createOperationDelegationSubmodelRepository(new HTTPOperationDelegation(allowlistedWebClient)); + + String submodelId = "allowlistedLoopbackDelegationSubmodel"; + OperationVariable[] inputOperationVariable = getInputVariable(); + String expectedResponse = getExpectedOutputResponse(getInputVariable()); + String path = "/operationInvocationAllowlisted"; + + createExpectationsForPost(path, getRequestBody(inputOperationVariable), expectedResponse, HttpStatusCode.OK_200); + createSubmodelAtRepository(allowlistedRepository, submodelId); + createInvokableSMEAtRepository(allowlistedRepository, submodelId, "operationDelegationSME", "http://127.0.0.1:2020" + path); + + OperationVariable[] actualOutputOperationVariable = allowlistedRepository.invokeOperation(submodelId, "operationDelegationSME", inputOperationVariable); + OperationVariable[] expectedOutputOperationVariable = getOutputVariable(getInputVariable()); + + assertArrayEquals(expectedOutputOperationVariable, actualOutputOperationVariable); + } + + @Test(expected = OperationDelegationException.class) + public void invokeOperationDelegationRejectsRedirectResponses() throws FileNotFoundException, IOException { + OperationVariable[] inputOperationVariable = getInputVariable(); + createExpectationsForPost("/operationInvocationRedirect", getRequestBody(inputOperationVariable), "[]", 302); + + String submodelId = "redirectDelegationSubmodel"; + createSubmodelAtRepository(submodelId); + createInvokableSMEAtRepository(submodelId, "operationDelegationSME", "http://localhost:2020/operationInvocationRedirect"); + + submodelRepository.invokeOperation(submodelId, "operationDelegationSME", inputOperationVariable); + } private OperationVariable[] getInputVariable() { return new OperationVariable[] { createIntOperationVariable("int") }; @@ -168,19 +241,35 @@ private static OperationVariable createOperationVariable(Property val) { return new DefaultOperationVariable.Builder().value(val).build(); } - private static WebClient createWebClient() { + private static WebClient createWebClient(OperationDelegationSecurityProperties securityProperties) { ExchangeStrategies strategies = ExchangeStrategies.builder().codecs(configurer -> { configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(configureObjectMapper())); configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(configureObjectMapper())); }).build(); - return WebClient.builder().exchangeStrategies(strategies).build(); + OperationDelegationTargetValidator targetValidator = new OperationDelegationTargetValidator(securityProperties); + return WebClient.builder().exchangeStrategies(strategies).filter((request, next) -> { + targetValidator.validate(request.url()); + return next.exchange(request); + }).build(); + } + + private static OperationDelegationSecurityProperties createSecurityPropertiesWithLocalhostAllowlist() { + OperationDelegationSecurityProperties securityProperties = new OperationDelegationSecurityProperties(); + securityProperties.getAllowlist().setHosts(Arrays.asList("localhost")); + securityProperties.getAllowlist().setCidrs(Arrays.asList("127.0.0.0/8", "::1/128")); + securityProperties.getAllowlist().setPorts(Arrays.asList(2020)); + return securityProperties; } private void createExpectationsForPost(String path, String requestBody, String expectedResponse, HttpStatusCode expectedResponseCode) throws FileNotFoundException, IOException { httpMockServer.createExpectationsForPostRequest(path, requestBody, expectedResponse, expectedResponseCode); } + private void createExpectationsForPost(String path, String requestBody, String expectedResponse, int expectedResponseCode) throws FileNotFoundException, IOException { + httpMockServer.createExpectationsForPostRequest(path, requestBody, expectedResponse, expectedResponseCode); + } + private static SubmodelRepository createOperationDelegationSubmodelRepository(OperationDelegation operationDelegation) { SubmodelRepositoryFactory repoFactory = CrudSubmodelRepositoryFactory.builder().backend(new InMemorySubmodelBackend()).fileRepository(new InMemoryFileRepository()).buildFactory(); @@ -192,9 +281,13 @@ private static Qualifier createInvocationDelegationQualifier(String delegationUR } private static void createSubmodelAtRepository(String submodelId) { + createSubmodelAtRepository(submodelRepository, submodelId); + } + + private static void createSubmodelAtRepository(SubmodelRepository repository, String submodelId) { Submodel submodel = createSubmodelDummy(submodelId); - submodelRepository.createSubmodel(submodel); + repository.createSubmodel(submodel); } private static Submodel createSubmodelDummy(String submodelId) { @@ -202,9 +295,13 @@ private static Submodel createSubmodelDummy(String submodelId) { } private static void createInvokableSMEAtRepository(String submodelId, String submodelElementIdShort, String delegationURL) { + createInvokableSMEAtRepository(submodelRepository, submodelId, submodelElementIdShort, delegationURL); + } + + private static void createInvokableSMEAtRepository(SubmodelRepository repository, String submodelId, String submodelElementIdShort, String delegationURL) { SubmodelElement submodelElement = new InvokableOperation.Builder().idShort(submodelElementIdShort).qualifiers(createInvocationDelegationQualifier(delegationURL)).build(); - submodelRepository.createSubmodelElement(submodelId, submodelElement); + repository.createSubmodelElement(submodelId, submodelElement); } private static DefaultOperationVariable createIntOperationVariable(String idShort) { diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationTargetValidator.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationTargetValidator.java new file mode 100644 index 000000000..c7e8d45c3 --- /dev/null +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/test/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/TestOperationDelegationTargetValidator.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (C) 2026 the Eclipse BaSyx Authors + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * SPDX-License-Identifier: MIT + ******************************************************************************/ + +package org.eclipse.digitaltwin.basyx.submodelrepository.feature.operation.delegation; + +import static org.junit.Assert.assertNotNull; + +import java.net.URI; +import java.util.Arrays; + +import org.eclipse.digitaltwin.basyx.core.exceptions.OperationDelegationException; +import org.junit.Test; + +public class TestOperationDelegationTargetValidator { + + @Test(expected = OperationDelegationException.class) + public void rejectUnsupportedScheme() { + OperationDelegationTargetValidator validator = new OperationDelegationTargetValidator(new OperationDelegationSecurityProperties()); + validator.validate(URI.create("ftp://example.org/operation")); + } + + @Test(expected = OperationDelegationException.class) + public void rejectMissingHost() { + OperationDelegationTargetValidator validator = new OperationDelegationTargetValidator(new OperationDelegationSecurityProperties()); + validator.validate(URI.create("http:///operation")); + } + + @Test(expected = OperationDelegationException.class) + public void rejectPrivateIpv4ByDefault() { + OperationDelegationTargetValidator validator = new OperationDelegationTargetValidator(new OperationDelegationSecurityProperties()); + validator.validate(URI.create("http://10.1.2.3/operation")); + } + + @Test + public void allowAllowlistedCidr() { + OperationDelegationSecurityProperties securityProperties = new OperationDelegationSecurityProperties(); + securityProperties.getAllowlist().setCidrs(Arrays.asList("10.0.0.0/8")); + + OperationDelegationTargetValidator validator = new OperationDelegationTargetValidator(securityProperties); + validator.validate(URI.create("http://10.1.2.3/operation")); + } + + @Test + public void allowAllowlistedHost() { + OperationDelegationSecurityProperties securityProperties = new OperationDelegationSecurityProperties(); + securityProperties.getAllowlist().setHosts(Arrays.asList("localhost")); + + OperationDelegationTargetValidator validator = new OperationDelegationTargetValidator(securityProperties); + validator.validate(URI.create("http://localhost/operation")); + } + + @Test(expected = OperationDelegationException.class) + public void rejectBlockedPortWhenPortAllowlistIsConfigured() { + OperationDelegationSecurityProperties securityProperties = new OperationDelegationSecurityProperties(); + securityProperties.getAllowlist().setPorts(Arrays.asList(443)); + + OperationDelegationTargetValidator validator = new OperationDelegationTargetValidator(securityProperties); + validator.validate(URI.create("http://8.8.8.8:80/operation")); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectInvalidCidrConfiguration() { + OperationDelegationSecurityProperties securityProperties = new OperationDelegationSecurityProperties(); + securityProperties.getAllowlist().setCidrs(Arrays.asList("10.0.0.0/not-a-prefix")); + new OperationDelegationTargetValidator(securityProperties); + } + + @Test + public void acceptValidPublicAddress() { + OperationDelegationTargetValidator validator = new OperationDelegationTargetValidator(new OperationDelegationSecurityProperties()); + validator.validate(URI.create("http://93.184.216.34/operation")); + assertNotNull(validator); + } +} diff --git a/basyx.submodelrepository/basyx.submodelrepository.component/src/main/resources/application.properties b/basyx.submodelrepository/basyx.submodelrepository.component/src/main/resources/application.properties index fd446506f..d82db1a55 100644 --- a/basyx.submodelrepository/basyx.submodelrepository.component/src/main/resources/application.properties +++ b/basyx.submodelrepository/basyx.submodelrepository.component/src/main/resources/application.properties @@ -39,6 +39,15 @@ basyx.backend = InMemory # This feature is enabled by default #basyx.submodelrepository.feature.operation.delegation.enabled = false +#basyx.submodelrepository.feature.operation.delegation.security.enabled = true +#basyx.submodelrepository.feature.operation.delegation.security.denyPrivateTargets = true +#basyx.submodelrepository.feature.operation.delegation.security.denyLinkLocalTargets = true +#basyx.submodelrepository.feature.operation.delegation.security.denyLoopbackTargets = true +#basyx.submodelrepository.feature.operation.delegation.security.denyMetadataTargets = true +#basyx.submodelrepository.feature.operation.delegation.security.denyRedirects = true +#basyx.submodelrepository.feature.operation.delegation.security.allowlist.hosts = localhost,*.trusted.example +#basyx.submodelrepository.feature.operation.delegation.security.allowlist.cidrs = 10.42.0.0/16,127.0.0.0/8,::1/128 +#basyx.submodelrepository.feature.operation.delegation.security.allowlist.ports = 443,8443 #################################################################################### # Feature: Search diff --git a/examples/BaSyxOperationDelegation/README.md b/examples/BaSyxOperationDelegation/README.md index ef4bd7afb..057f55478 100644 --- a/examples/BaSyxOperationDelegation/README.md +++ b/examples/BaSyxOperationDelegation/README.md @@ -1,22 +1,32 @@ # BaSyx Operation Delegation Example Setup + To run the example containers, you need to have Docker installed on your device. ## How to start the example containers + 1. Open a terminal in this folder 2. Run the following command to start the BaSyx containers: -``` + +```bash docker-compose up -d ``` +## Security note + +Operation delegation now validates outbound delegation targets with secure defaults. +This example delegates to the Docker-internal host example-operation-service:8080, so the environment configuration includes an explicit allowlist entry in [examples/BaSyxOperationDelegation/basyx/aas-env.properties](examples/BaSyxOperationDelegation/basyx/aas-env.properties). + ## Access the BaSyx containers + - AAS Environment: [http://localhost:8081](http://localhost:8081) - AAS Registry: [http://localhost:8082](http://localhost:8082) - Submodel Registry: [http://localhost:8083](http://localhost:8083) -- AAS Web GUI: [http://localhost:3000](http://localhost:3000) -- Delegated Operation Service [http://localhost:8087](http://localhost:8087) +- AAS Web GUI: [http://localhost:3000](http://localhost:3000) +- Delegated Operation Service [http://localhost:8087](http://localhost:8087) ## How to use + 1. Open the AAS Web GUI -![Screenshot of the AAS Web GUI showcasing the Operation](AASWebGUI.png) + ![Screenshot of the AAS Web GUI showcasing the Operation](AASWebGUI.png) 2. Open the Operation -3. Enter a value and execute \ No newline at end of file +3. Enter a value and execute diff --git a/examples/BaSyxOperationDelegation/basyx/aas-env.properties b/examples/BaSyxOperationDelegation/basyx/aas-env.properties index 3d3d08d9d..fc1521dff 100644 --- a/examples/BaSyxOperationDelegation/basyx/aas-env.properties +++ b/examples/BaSyxOperationDelegation/basyx/aas-env.properties @@ -6,3 +6,5 @@ basyx.cors.allowed-methods=GET,POST,PATCH,DELETE,PUT,OPTIONS,HEAD basyx.aasrepository.feature.registryintegration=http://aas-registry:8080 basyx.submodelrepository.feature.registryintegration=http://sm-registry:8080 basyx.externalurl=http://localhost:8081 +basyx.submodelrepository.feature.operation.delegation.security.allowlist.hosts=example-operation-service +basyx.submodelrepository.feature.operation.delegation.security.allowlist.ports=8080 diff --git a/examples/BaSyxOperationDelegation/docker-compose.yml b/examples/BaSyxOperationDelegation/docker-compose.yml index 8cc9f78de..e70d4e912 100644 --- a/examples/BaSyxOperationDelegation/docker-compose.yml +++ b/examples/BaSyxOperationDelegation/docker-compose.yml @@ -1,6 +1,6 @@ services: aas-env: - image: eclipsebasyx/aas-environment:2.0.0-milestone-04 + image: eclipsebasyx/aas-environment:2.0.0-SNAPSHOT container_name: aas-env volumes: - ./aas:/application/aas @@ -14,7 +14,7 @@ services: sm-registry: condition: service_healthy aas-registry: - image: eclipsebasyx/aas-registry-log-mem:2.0.0-milestone-04 + image: eclipsebasyx/aas-registry-log-mem:2.0.0-SNAPSHOT container_name: aas-registry ports: - '8082:8080' @@ -22,7 +22,7 @@ services: - ./basyx/aas-registry.yml:/workspace/config/application.yml restart: always sm-registry: - image: eclipsebasyx/submodel-registry-log-mem:2.0.0-milestone-04 + image: eclipsebasyx/submodel-registry-log-mem:2.0.0-SNAPSHOT container_name: sm-registry ports: - '8083:8080' From ebc626ed7f3f10b2d68e716d1d0c0c6b36255bcd Mon Sep 17 00:00:00 2001 From: Aaron Zielstorff Date: Thu, 23 Apr 2026 20:26:17 +0200 Subject: [PATCH 5/5] Fixes failing tests --- .../aasservice/feature/mqtt/TestMqttAasService.java | 6 ++---- .../service/tests/integration/BaseIntegrationTest.java | 10 ++++++++-- .../authorization/AuthorizedSubmodelRepository.java | 5 +++++ .../feature/kafka/KafkaSubmodelRepository.java | 5 +++++ .../feature/mqtt/MqttSubmodelRepository.java | 5 +++++ .../OperationDelegationSubmodelRepository.java | 5 +++++ .../RegistryIntegrationSubmodelRepository.java | 5 +++++ .../feature/search/SearchSubmodelRepository.java | 5 +++++ 8 files changed, 40 insertions(+), 6 deletions(-) diff --git a/basyx.aasservice/basyx.aasservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/TestMqttAasService.java b/basyx.aasservice/basyx.aasservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/TestMqttAasService.java index b60a1b97f..ff35af98e 100644 --- a/basyx.aasservice/basyx.aasservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/TestMqttAasService.java +++ b/basyx.aasservice/basyx.aasservice-feature-mqtt/src/test/java/org/eclipse/digitaltwin/basyx/aasservice/feature/mqtt/TestMqttAasService.java @@ -46,8 +46,8 @@ import org.eclipse.digitaltwin.basyx.aasservice.backend.InMemoryAasBackend; import org.eclipse.digitaltwin.basyx.common.mqttcore.encoding.URLEncoder; import org.eclipse.digitaltwin.basyx.common.mqttcore.listener.MqttTestListener; -import org.eclipse.digitaltwin.basyx.core.filerepository.FileMetadata; import org.eclipse.digitaltwin.basyx.core.filerepository.FileRepository; +import org.eclipse.digitaltwin.basyx.core.filerepository.FileRepositoryHelper; import org.eclipse.digitaltwin.basyx.core.filerepository.InMemoryFileRepository; import org.eclipse.digitaltwin.basyx.http.Aas4JHTTPSerializationExtension; import org.eclipse.digitaltwin.basyx.http.BaSyxHTTPConfiguration; @@ -109,9 +109,7 @@ protected AasService getAasServiceWithThumbnail() throws IOException { AssetAdministrationShell expected = DummyAssetAdministrationShellFactory.createForThumbnail(); AasService aasServiceWithThumbnail = getAasService(expected); - FileMetadata defaultThumbnail = new FileMetadata("dummyImgA.jpeg", "", createDummyImageIS_A()); - - String thumbnailFilePath = fileRepository.save(defaultThumbnail); + String thumbnailFilePath = FileRepositoryHelper.saveOrOverwriteFile(fileRepository, "dummyImgA.jpeg", "", createDummyImageIS_A()); Resource defaultResource = new DefaultResource.Builder().path(thumbnailFilePath).contentType("").build(); AssetInformation defaultAasAssetInformation = aasServiceWithThumbnail.getAssetInformation(); diff --git a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java index dbd7d0ba4..bf120856d 100644 --- a/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java +++ b/basyx.submodelregistry/basyx.submodelregistry-service-basetests/src/main/java/org/eclipse/digitaltwin/basyx/submodelregistry/service/tests/integration/BaseIntegrationTest.java @@ -143,8 +143,14 @@ protected void cleanup() throws ApiException, InterruptedException, Deserializat adapter.assertNoAdditionalMessages(); GetSubmodelDescriptorsResult result = api.getAllSubmodelDescriptors(null, null); for (SubmodelDescriptor eachDescriptor : result.getResult()) { - api.deleteSubmodelDescriptorById(eachDescriptor.getId()); - assertThatEventWasSend(RegistryEvent.builder().id(eachDescriptor.getId()).type(EventType.SUBMODEL_UNREGISTERED).build()); + try { + api.deleteSubmodelDescriptorById(eachDescriptor.getId()); + assertThatEventWasSend(RegistryEvent.builder().id(eachDescriptor.getId()).type(EventType.SUBMODEL_UNREGISTERED).build()); + } catch (ApiException e) { + if (e.getCode() != NOT_FOUND) { + throw e; + } + } } } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java index 5820ced5c..74497804d 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-authorization/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/authorization/AuthorizedSubmodelRepository.java @@ -309,6 +309,11 @@ public InputStream getFileByFilePath(String submodelId, String filePath) { return decorated.getFileByFilePath(submodelId, filePath); } + @Override + public String getOriginalFileNameByPath(String submodelId, String idShortPath) { + return decorated.getOriginalFileNameByPath(submodelId, idShortPath); + } + private List getIdAsList(String id) { return new ArrayList<>(Arrays.asList(id)); } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepository.java index d75b53126..8aa5f7800 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-kafka/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/kafka/KafkaSubmodelRepository.java @@ -196,6 +196,11 @@ public InputStream getFileByFilePath(String submodelId, String filePath) { return decorated.getFileByFilePath(submodelId, filePath); } + @Override + public String getOriginalFileNameByPath(String submodelId, String idShortPath) { + return decorated.getOriginalFileNameByPath(submodelId, idShortPath); + } + @Override public CursorResult> getAllSubmodels(String semanticId, PaginationInfo pInfo) { return decorated.getAllSubmodels(semanticId, pInfo); diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java index d824ab92c..5ca4f42ea 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-mqtt/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/mqtt/MqttSubmodelRepository.java @@ -202,6 +202,11 @@ public InputStream getFileByFilePath(String submodelId, String filePath) { return decorated.getFileByFilePath(submodelId, filePath); } + @Override + public String getOriginalFileNameByPath(String submodelId, String idShortPath) { + return decorated.getOriginalFileNameByPath(submodelId, idShortPath); + } + private void submodelCreated(Submodel submodel, String repoId) { sendMqttMessage(topicFactory.createCreateSubmodelTopic(repoId), SubmodelSerializer.serializeSubmodel(submodel)); } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java index 907e9b8de..fd7e09601 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-operation-delegation/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/operation/delegation/OperationDelegationSubmodelRepository.java @@ -184,4 +184,9 @@ public InputStream getFileByFilePath(String submodelId, String filePath) { return decorated.getFileByFilePath(submodelId, filePath); } + @Override + public String getOriginalFileNameByPath(String submodelId, String idShortPath) { + return decorated.getOriginalFileNameByPath(submodelId, idShortPath); + } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java index 68b301f35..c1cf57654 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-registry-integration/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/registry/integration/RegistryIntegrationSubmodelRepository.java @@ -233,4 +233,9 @@ public InputStream getFileByFilePath(String submodelId, String filePath) { return decorated.getFileByFilePath(submodelId, filePath); } + @Override + public String getOriginalFileNameByPath(String submodelId, String idShortPath) { + return decorated.getOriginalFileNameByPath(submodelId, idShortPath); + } + } diff --git a/basyx.submodelrepository/basyx.submodelrepository-feature-search/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/search/SearchSubmodelRepository.java b/basyx.submodelrepository/basyx.submodelrepository-feature-search/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/search/SearchSubmodelRepository.java index c05a2300e..ffacd6693 100644 --- a/basyx.submodelrepository/basyx.submodelrepository-feature-search/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/search/SearchSubmodelRepository.java +++ b/basyx.submodelrepository/basyx.submodelrepository-feature-search/src/main/java/org/eclipse/digitaltwin/basyx/submodelrepository/feature/search/SearchSubmodelRepository.java @@ -183,6 +183,11 @@ public InputStream getFileByFilePath(String submodelId, String filePath) { return decorated.getFileByFilePath(submodelId, filePath); } + @Override + public String getOriginalFileNameByPath(String submodelId, String idShortPath) { + return decorated.getOriginalFileNameByPath(submodelId, idShortPath); + } + private void indexSM(Submodel submodel) { try { JsonNode normalizedSubmodel = IndexNormalizer.toIndexable(submodel);