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 7 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 @@ -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
@@ -0,0 +1,78 @@
package de.sovity.edc.utils.catalog.mapper;

import de.sovity.edc.utils.JsonUtils;
import de.sovity.edc.utils.jsonld.JsonLdUtils;
import de.sovity.edc.utils.jsonld.vocab.Prop;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import lombok.val;
import org.eclipse.edc.connector.contract.spi.ContractId;
import org.jetbrains.annotations.NotNull;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class DspContractOfferUtils {

/**
* /!\ Workaround
* <p>
* 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.
*
* @param contract The contract to compute an ID from
* @return A base64 string that can be used as an id for the {@code contract}
*/
public static String buildStableId(JsonObject contract) {
// 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(contract).remove(Prop.ID).build();
val policyId = hash(noId);

val currentId = ContractId.parseId(JsonLdUtils.string(contract, Prop.ID))
.orElseThrow((failure) -> {
throw new RuntimeException("Failed to parse the contract id: " + failure.getFailureDetail());
});

return currentId.definitionPart() + ":" + currentId.assetIdPart() + ":" + policyId;
}

@NotNull
private static String hash(JsonObject noId) {
val policyJsonString = JsonUtils.toJson(noId);
val sha1 = sha1(policyJsonString);
// encoding with base16 to make the hash readable to humans (similarly to how the random UUID would have been readable)
val base16 = toBase16(sha1);
return toBase64(base16);
}

@NotNull
private static String toBase64(String string) {
byte[] stringBytes = string.getBytes(StandardCharsets.UTF_8);
byte[] bytes = Base64.getEncoder().encode(stringBytes);
return new String(bytes);
}

@NotNull
private static String toBase16(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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import jakarta.json.Json;
import jakarta.json.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.eclipse.edc.jsonld.spi.JsonLd;
import org.jetbrains.annotations.NotNull;

Expand Down Expand Up @@ -66,6 +67,9 @@ private DspDataOffer buildDataOffer(JsonObject dataset) {

@NotNull
private DspContractOffer buildContractOffer(JsonObject json) {
return new DspContractOffer(JsonLdUtils.id(json), json);
val stableId = DspContractOfferUtils.buildStableId(json);
val withStableId = Json.createObjectBuilder(json).remove(Prop.ID).add(Prop.ID, stableId).build();

return new DspContractOffer(stableId, withStableId);
}
}
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));

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,24 @@
package de.sovity.edc.utils.catalog.mapper;

import jakarta.json.Json;
import lombok.val;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

class DspContractOfferUtilsTest {
@Test
void testCanConvertTheRandomIdToStableId() {
// arrange
val contractOffer = Json.createObjectBuilder()
.add("@id", "part1:part2:part3")
.add("somefield", "somevalue")
.build();

// act
val result = DspContractOfferUtils.buildStableId(contractOffer);

// assert
assertThat(result).isEqualTo("part1:part2:MjliNzcwMjdjMzA2YzE3ZGYyZWNhZjY2OGI3OTYxY2U5OTY1YmExNw==");
}
}
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