Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add stable contract offer id #795

Merged
merged 8 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ please see [changelog_updates.md](docs/dev/changelog_updates.md).

#### Patch Changes
- Docs: Enhanced starting a Http-Pull over the EDC-Ui documentation
- DspCatalogService: Contract Offer IDs are now stable
- Fix Connector-Restricted-Usage Policy
- Fix Connection-Pool issues by switching to the Tractus-X Connection Pool.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

package de.sovity.edc.utils.catalog.mapper;

import de.sovity.edc.utils.JsonUtils;
import de.sovity.edc.utils.catalog.DspCatalogServiceException;
import de.sovity.edc.utils.catalog.model.DspCatalog;
import de.sovity.edc.utils.catalog.model.DspContractOffer;
Expand All @@ -22,13 +23,22 @@
import jakarta.json.Json;
import jakarta.json.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.eclipse.edc.connector.contract.spi.ContractId;
import org.eclipse.edc.jsonld.spi.JsonLd;
import org.eclipse.edc.spi.monitor.Monitor;
import org.jetbrains.annotations.NotNull;

import java.nio.charset.StandardCharsets;
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);
Expand Down Expand Up @@ -65,7 +75,66 @@ private DspDataOffer buildDataOffer(JsonObject dataset) {
}

@NotNull
private DspContractOffer buildContractOffer(JsonObject json) {
return new DspContractOffer(JsonLdUtils.id(json), json);
DspContractOffer buildContractOffer(JsonObject json) {
/*
* /!\ Workaround
*
* The Eclipse EDC uses a new random UUID for each policy that it returns and in turn a new contract ID.
* This Eclipse ID can't be used as such.
* 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.
*/
ununhexium marked this conversation as resolved.
Show resolved Hide resolved

// 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(Prop.ID).build();
val hash = hashJsonObject(noId);
val policyId = stableContractId(hash);
ununhexium marked this conversation as resolved.
Show resolved Hide resolved

val idAsString = JsonLdUtils.string(json, Prop.ID);

val stableId = ContractId
.parseId(idAsString)
.map(it -> it.definitionPart() + ":" + it.assetIdPart() + ":" + policyId)
.orElseThrow((failure) -> {
throw new RuntimeException("Failed to parse the contract id: " + failure.getFailureDetail());
});
ununhexium marked this conversation as resolved.
Show resolved Hide resolved

val copy = Json.createObjectBuilder(json).remove(Prop.ID).add(Prop.ID, stableId).build();

return new DspContractOffer(stableId, copy);
}

@NotNull
private String hashJsonObject(JsonObject noId) {
val policyJsonString = JsonUtils.toJson(noId);
val sha1 = sha1(policyJsonString);
// encoding with base16 to make the hash readable to humans when decoding with ContractId
return toBase16String(sha1);
}

private static String stableContractId(String string) {
ununhexium marked this conversation as resolved.
Show resolved Hide resolved
byte[] stringBytes = string.getBytes(StandardCharsets.UTF_8);
byte[] bytes = Base64.getEncoder().encode(stringBytes);
return new String(bytes);
}

@NotNull
private static String toBase16String(byte[] bytes) {
val sb = new StringBuilder();
for (byte b : bytes) {
sb.append(Character.forDigit(b >> 4 & 0xf, 16));
sb.append(Character.forDigit(b & 0xf, 16));
}
return sb.toString();
}

private static byte[] sha1(String string) {
try {
return MessageDigest.getInstance("sha-1").digest(string.getBytes());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
richardtreier marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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:ODJjMDNjMmM4YzQ5NmE0OTg4ZjVjOTY4OGU2MmMyN2UxYjIxZDAwZQ==");
assertThat(toJson(co.getPolicyJsonLd())).contains("ALWAYS_TRUE");

assertThat(offer.getDistributions()).hasSize(1);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package de.sovity.edc.utils.catalog.mapper;

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.mockito.Mockito.mock;

class DspDataOfferBuilderTest {
@Test
void testCanConvertTheRandomIdToStableId() {
// 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:MjliNzcwMjdjMzA2YzE3ZGYyZWNhZjY2OGI3OTYxY2U5OTY1YmExNw==";
assertThat(result.getContractOfferId()).isEqualTo(stableId);
assertThat(result.getPolicyJsonLd().getString("@id")).isEqualTo(stableId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading