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 1 commit
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 @@ -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
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 @@ -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);
Expand Down Expand Up @@ -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);
}
}
}
}
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:gsA8LIxJakmI9clojmLCfhsh0A4=");
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:KbdwJ8MGwX3y7K9mi3lhzplluhc=";
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