Skip to content

Commit

Permalink
feat: stable contract offer id in DspCatalogService (sovity#795)
Browse files Browse the repository at this point in the history
  • Loading branch information
Christophe Loiseau authored and dhommen committed Mar 25, 2024
1 parent dd6308b commit b30c350
Show file tree
Hide file tree
Showing 7 changed files with 112 additions and 6 deletions.
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).
- Add new MDS fields and migrate existing MDS asset keys to mobilityDCAT-AP

#### Patch Changes
- DspCatalogService: Contract Offer IDs are now stable

### Deployment Migration Notes
- new logging-house-client env for mds:
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

0 comments on commit b30c350

Please sign in to comment.