From 3ef07d0cec51d115c02fd9cb28de99d3598215e2 Mon Sep 17 00:00:00 2001 From: Christophe '116' Loiseau <116@lab0.net> Date: Tue, 13 Feb 2024 11:43:28 +0100 Subject: [PATCH] Add stable contract offer id --- CHANGELOG.md | 1 + .../WrapperExtensionContextBuilder.java | 2 +- .../ContractAgreementPageCardBuilder.java | 2 - .../catalog/mapper/DspDataOfferBuilder.java | 56 ++++++++++++++++++- .../utils/catalog/DspCatalogServiceTest.java | 5 +- .../mapper/DspDataOfferBuilderTest.java | 32 +++++++++++ .../edc/utils/catalog/catalogResponse.json | 2 +- 7 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilderTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b8396339..522888d11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md). #### Minor Changes - Add new MDS fields and migrate existing MDS asset keys to mobilityDCAT-AP +- Introduce a stable ID for contracts offers #### Patch Changes - Docs: Enhanced starting a Http-Pull over the EDC-Ui documentation diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java index eaf7190d3..c05734c5a 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/WrapperExtensionContextBuilder.java @@ -206,7 +206,7 @@ public static WrapperExtensionContext buildContext( policyDefinitionService, policyMapper ); - var dataOfferBuilder = new DspDataOfferBuilder(jsonLd); + var dataOfferBuilder = new DspDataOfferBuilder(jsonLd, monitor); var dspCatalogService = new DspCatalogService(catalogService, dataOfferBuilder); var catalogApiService = new CatalogApiService( assetMapper, diff --git a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java index bc8fa989b..d090c4401 100644 --- a/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java +++ b/extensions/wrapper/wrapper/src/main/java/de/sovity/edc/ext/wrapper/api/ui/pages/contract_agreements/services/ContractAgreementPageCardBuilder.java @@ -22,7 +22,6 @@ import de.sovity.edc.ext.wrapper.api.ui.pages.transferhistory.TransferProcessStateService; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.eclipse.edc.connector.contract.spi.types.agreement.ContractAgreement; import org.eclipse.edc.connector.contract.spi.types.negotiation.ContractNegotiation; import org.eclipse.edc.connector.transfer.spi.types.TransferProcess; @@ -35,7 +34,6 @@ import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcMillisToOffsetDateTime; import static de.sovity.edc.ext.wrapper.utils.EdcDateUtils.utcSecondsToOffsetDateTime; -@Slf4j @RequiredArgsConstructor public class ContractAgreementPageCardBuilder { private final PolicyMapper policyMapper; diff --git a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java index 119d9d102..139459e88 100644 --- a/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java +++ b/utils/catalog-parser/src/main/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilder.java @@ -21,14 +21,23 @@ import de.sovity.edc.utils.jsonld.vocab.Prop; import jakarta.json.Json; import jakarta.json.JsonObject; +import jakarta.json.JsonString; import lombok.RequiredArgsConstructor; +import lombok.val; import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.spi.monitor.Monitor; import org.jetbrains.annotations.NotNull; +import java.io.StringWriter; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + @RequiredArgsConstructor public class DspDataOfferBuilder { private final JsonLd jsonLd; + private final Monitor monitor; public DspCatalog buildDataOffers(String endpoint, JsonObject json) { json = jsonLd.expand(json).orElseThrow(DspCatalogServiceException::ofFailure); @@ -65,7 +74,50 @@ private DspDataOffer buildDataOffer(JsonObject dataset) { } @NotNull - private DspContractOffer buildContractOffer(JsonObject json) { - return new DspContractOffer(JsonLdUtils.id(json), json); + DspContractOffer buildContractOffer(JsonObject json) { + /* + * /!\ Workaround + * TODO: can't reference a private repo in a public repo + * https://github.com/sovity/edc-broker-server-extension/issues/278 + * https://github.com/sovity/edc-broker-server-extension/issues/409 + * + * The Eclipse EDC uses a new random ID for each contract offer that it returns. + * This can't be used as an id. + * As a workaround, we must introduce our own ID. + * For a first iteration, we will assume that the content of the policy remains the same (same content, same order) + * and hash it to use it as a key. + */ + + String idFieldName = "@id"; + val id = json.get(idFieldName); + val idAsString = (JsonString) id; + val parts = idAsString.getString().split(":"); + if (parts.length != 3) { + throw new RuntimeException("Can't use " + idAsString + ": wrong format, must be made of 3 parts."); + } + + val sw = new StringWriter(); + try (val writer = Json.createWriter(sw)) { + // FIXME: This doesn't enforce any property order and may cause trouble if the returned policy schema is not consistent + // Use canonical form if needed later. + val noId = Json.createObjectBuilder(json).remove(idFieldName).build(); + writer.write(noId); + val policyJsonString = sw.toString(); + try { + val hash = MessageDigest.getInstance("sha-1").digest(policyJsonString.getBytes()); + val b64 = Base64.getEncoder().encode(hash); + val contractId = parts[0]; + val assetId = parts[1]; + val policyId = new String(b64); + val stableId = contractId + ":" + assetId + ":" + policyId; + + val copy = Json.createObjectBuilder(json).remove(idFieldName).add(idFieldName, stableId).build(); + + return new DspContractOffer(stableId, copy); + } catch (NoSuchAlgorithmException e) { + monitor.severe("Failed to hash with sha-1", e); + throw new RuntimeException(e); + } + } } } diff --git a/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java b/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java index 1124f8405..964a029d7 100644 --- a/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java +++ b/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/DspCatalogServiceTest.java @@ -45,7 +45,8 @@ private DspCatalogService newDspCatalogService(String resultJsonFilename) { var result = CompletableFuture.completedFuture(StatusResult.success(catalogJson.getBytes(StandardCharsets.UTF_8))); when(catalogService.requestCatalog(eq(endpoint), eq("dataspace-protocol-http"), eq(QuerySpec.max()))).thenReturn(result); - var dataOfferBuilder = new DspDataOfferBuilder(new TitaniumJsonLd(mock(Monitor.class))); + var monitor = mock(Monitor.class); + var dataOfferBuilder = new DspDataOfferBuilder(new TitaniumJsonLd(monitor), monitor); return new DspCatalogService(catalogService, dataOfferBuilder); } @@ -71,7 +72,7 @@ void testCatalogMapping() { assertThat(offer.getContractOffers()).hasSize(1); var co = offer.getContractOffers().get(0); - assertThat(co.getContractOfferId()).isEqualTo("policy-1"); + assertThat(co.getContractOfferId()).isEqualTo("contract-id:asset-id:gsA8LIxJakmI9clojmLCfhsh0A4="); assertThat(toJson(co.getPolicyJsonLd())).contains("ALWAYS_TRUE"); assertThat(offer.getDistributions()).hasSize(1); diff --git a/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilderTest.java b/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilderTest.java new file mode 100644 index 000000000..a2f91d4f1 --- /dev/null +++ b/utils/catalog-parser/src/test/java/de/sovity/edc/utils/catalog/mapper/DspDataOfferBuilderTest.java @@ -0,0 +1,32 @@ +package de.sovity.edc.utils.catalog.mapper; + +import com.apicatalog.jsonld.JsonLd; +import jakarta.json.Json; +import lombok.val; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.spi.monitor.Monitor; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +class DspDataOfferBuilderTest { + @Test + void testCanConvertTheEdcIdToAStableId() { + // arrange + val contractOffer = Json.createObjectBuilder() + .add("@id", "part1:part2:part3") + .add("somefield", "somevalue") + .build(); + + // act + val monitor = mock(Monitor.class); + val result = new DspDataOfferBuilder(new TitaniumJsonLd(monitor), monitor).buildContractOffer(contractOffer); + + // assert + val stableId = "part1:part2:KbdwJ8MGwX3y7K9mi3lhzplluhc="; + assertThat(result.getContractOfferId()).isEqualTo(stableId); + assertThat(result.getPolicyJsonLd().getString("@id")).isEqualTo(stableId); + } +} diff --git a/utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json b/utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json index d069f7302..613b7ac49 100644 --- a/utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json +++ b/utils/catalog-parser/src/test/resources/de/sovity/edc/utils/catalog/catalogResponse.json @@ -5,7 +5,7 @@ "@id": "test-1.0", "@type": "dcat:Dataset", "odrl:hasPolicy": { - "@id": "policy-1", + "@id": "contract-id:asset-id:policy-id", "@type": "odrl:Set", "odrl:permission": { "odrl:target": "test-1.0",