Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,20 @@ to select the

## Transfer Data

Now that both participants are set up, we can transfer data from the Provider to the Consumer.
There are two use case supported here:

- Http proxy
- Certificates sharing via HTTP

### Http proxy

EDC-V offers a one-stop-shop API to transfer data. This is achieved by two endpoints, one that fetches the catalog (
`Data Transfer/Get Catalog`) and another endpoint (`Data Transfer/Get Data`) that initiates the contract negotiation,
`Data Transfer/Http Todo/Get Catalog`) and another endpoint (`Data Transfer/Http Todo/Get Data`) that initiates the
contract negotiation,
waits for its successful completion, then starts the data transfer.

Perform the entire sequence by running both requests in the `Data Transfer` folder in Bruno:
Perform the entire sequence by running both requests in the `Data Transfer/Http Todo` folder in Bruno:

![img.png](docs/images/bruno_transfer.png)

Expand All @@ -297,6 +306,30 @@ from https://jsonplaceholder.typicode.com/todos, something like:
]
```

### Certificates sharing via HTTP

The second use case demonstrates how certificates can be shared between participants using EDC-V's HTTP data
transfer capabilities.

First we need to upload a certificate to the Provider. This is done by running the
`Data Transfer/Http Certs/Provider/Upload Certificate` request in Bruno:

![img.png](docs/images/bruno_upload_certificate.png)

by selecting a file to upload (e.g. a `.pdf` file). Additional metadata can be provided in the request body using
the `metadata` field.

Then perform the entire sequence by running both requests in the `Data Transfer/Http Certs/Consumer` folder in Bruno:

![img.png](docs/images/bruno_certificate_consumer.png)

which:

- Fetches the catalog from the Provider storing the offer id for the certificate asset
- Setup the transfer request using the offer id (contract negotiation + transfer initiation) storing the access token
- Query the provider for listing the available certificates storing the first certificate id
- Finally, download the certificate using the certificate id

## Automated tests

JAD comes with a set of automated tests that can be run against the deployed services. These tests are located in the
Expand Down
Binary file added docs/images/bruno_certificate_consumer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/images/bruno_transfer.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/bruno_upload_certificate.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,32 @@ public void getData(@PathParam("participantContextId") String participantContext
});


}

@POST
@Path("/transfer")
public void setupTransfer(@PathParam("participantContextId") String participantContextId, DataRequest dataRequest, @Suspended AsyncResponse response) {
var participantContext = participantContextService.getParticipantContext(participantContextId);
if (participantContext.failed()) {
response.resume(Response.status(404).entity("Participant context '%s' not found".formatted(participantContextId)).build());
}
dataRequestService.setupTransfer(participantContext.getContent(), dataRequest)
.whenComplete((result, throwable) -> {
try {
if (throwable != null) {
response.resume(Response.status(500).entity(throwable.getMessage()).build());

} else if (result.succeeded()) {
response.resume(result.getContent());
} else {
response.resume(Response.status(500).entity(result.getFailureDetail()).build());
}
} catch (Throwable mapped) {
response.resume(Response.status(500).entity(mapped.getMessage()).build());
}
});


}

private <T> T toResponse(StatusResult<T> result, Throwable throwable) throws Throwable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import static java.net.http.HttpClient.newHttpClient;
Expand Down Expand Up @@ -71,6 +72,16 @@ public CompletableFuture<ServiceResult<Object>> getData(ParticipantContext parti
.thenApply(ServiceResult::success);
}

public CompletableFuture<ServiceResult<Map<String, Object>>> setupTransfer(ParticipantContext participantContext, DataRequest dataRequest) {
return initiateContractNegotiation(participantContext, dataRequest)
.thenCompose(this::waitForContractNegotiation)
.thenCompose(contractNegotiation -> startTransferProcess(participantContext, contractNegotiation))
.thenCompose(this::waitForTransferProcess)
.thenCompose(transferProcess -> getEdr(transferProcess.getId()))
.thenCompose(edr -> CompletableFuture.completedFuture(edr.getProperties()))
.thenApply(ServiceResult::success);
}

private CompletableFuture<String> initiateContractNegotiation(ParticipantContext participantContext, DataRequest dataRequest) {
var addressForDid = getAddressForDid(dataRequest.providerId());
if (addressForDid.failed()) {
Expand Down
45 changes: 45 additions & 0 deletions extensions/data-plane-certs/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 Metaform Systems, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Metaform Systems, Inc. - initial API and implementation
*
*/

plugins {
`java-library`
id(libs.plugins.swagger.get().pluginId)
}

dependencies {
api(libs.edc.spi.http)
api(libs.edc.spi.transaction)
api(libs.edc.spi.web)
api(libs.edc.spi.dataplane)
implementation(libs.jersey.multipart)
implementation(libs.edc.lib.util)
implementation(libs.edc.lib.sql)
implementation(libs.edc.core.sql.bootstrapper)
implementation(libs.edc.lib.util.dataplane)
implementation(libs.edc.dataplane.iam)
implementation(libs.jakarta.rsApi)

testImplementation(libs.edc.lib.http)
testImplementation(libs.edc.junit)
testImplementation(libs.restAssured)
testImplementation(testFixtures(libs.edc.core.jersey))

}
edcBuild {
swagger {
apiGroup.set("public-api")
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2025 Metaform Systems, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Metaform Systems, Inc. - initial API and implementation
*
*/

package org.eclipse.edc.virtualized.dataplane.cert;

import org.eclipse.edc.connector.dataplane.iam.service.DataPlaneAuthorizationServiceImpl;
import org.eclipse.edc.connector.dataplane.spi.Endpoint;
import org.eclipse.edc.connector.dataplane.spi.edr.EndpointDataReferenceServiceRegistry;
import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAuthorizationService;
import org.eclipse.edc.connector.dataplane.spi.iam.PublicEndpointGeneratorService;
import org.eclipse.edc.runtime.metamodel.annotation.Configuration;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.runtime.metamodel.annotation.Settings;
import org.eclipse.edc.spi.system.Hostname;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.transaction.spi.TransactionContext;
import org.eclipse.edc.virtualized.dataplane.cert.api.CertExchangePublicController;
import org.eclipse.edc.virtualized.dataplane.cert.api.CertInternalExchangeController;
import org.eclipse.edc.virtualized.dataplane.cert.store.CertStore;
import org.eclipse.edc.web.spi.WebService;
import org.eclipse.edc.web.spi.configuration.PortMapping;
import org.eclipse.edc.web.spi.configuration.PortMappingRegistry;

import static org.eclipse.edc.virtualized.dataplane.cert.CertExchangeExtension.NAME;

@Extension(NAME)
public class CertExchangeExtension implements ServiceExtension {
public static final String NAME = "Cert Exchange Extension";
public static final String API_CONTEXT = "certs";
private static final int DEFAULT_CERTS_PORT = 8186;
private static final String DEFAULT_CERTS_PATH = "/api/data";


@Setting(description = "Base url of the public public API endpoint without the trailing slash. This should point to the public certs endpoint configured.",
required = false,
key = "edc.dataplane.api.certs.baseurl", warnOnMissingConfig = true)
private String publicBaseUrl;

@Configuration
private CertApiConfiguration apiConfiguration;

@Inject
private Hostname hostname;

@Inject
private PortMappingRegistry portMappingRegistry;

@Inject
private DataPlaneAuthorizationService authorizationService;
@Inject
private PublicEndpointGeneratorService generatorService;

@Inject
private EndpointDataReferenceServiceRegistry endpointDataReferenceServiceRegistry;

@Inject
private WebService webService;

@Inject
private CertStore certStore;

@Inject
private TransactionContext transactionContext;

@Override
public void initialize(ServiceExtensionContext context) {
var portMapping = new PortMapping(API_CONTEXT, apiConfiguration.port(), apiConfiguration.path());
portMappingRegistry.register(portMapping);

if (publicBaseUrl == null) {
publicBaseUrl = "http://%s:%d%s".formatted(hostname.get(), portMapping.port(), portMapping.path());
context.getMonitor().warning("The public API endpoint was not explicitly configured, the default '%s' will be used.".formatted(publicBaseUrl));
}
var endpoint = Endpoint.url(publicBaseUrl);
generatorService.addGeneratorFunction("HttpCertData", dataAddress -> endpoint);
webService.registerResource(API_CONTEXT, new CertExchangePublicController(authorizationService, certStore, transactionContext));
webService.registerResource("control", new CertInternalExchangeController(certStore, transactionContext));

if (authorizationService instanceof DataPlaneAuthorizationServiceImpl dpAuthService) {
endpointDataReferenceServiceRegistry.register("HttpCertData", dpAuthService);
}
}

@Settings
record CertApiConfiguration(
@Setting(key = "web.http." + API_CONTEXT + ".port", description = "Port for " + API_CONTEXT + " api context", defaultValue = DEFAULT_CERTS_PORT + "")
int port,
@Setting(key = "web.http." + API_CONTEXT + ".path", description = "Path for " + API_CONTEXT + " api context", defaultValue = DEFAULT_CERTS_PATH)
String path
) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2025 Metaform Systems, Inc.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Metaform Systems, Inc. - initial API and implementation
*
*/

package org.eclipse.edc.virtualized.dataplane.cert;

import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.sql.QueryExecutor;
import org.eclipse.edc.sql.bootstrapper.SqlSchemaBootstrapper;
import org.eclipse.edc.transaction.datasource.spi.DataSourceRegistry;
import org.eclipse.edc.transaction.spi.TransactionContext;
import org.eclipse.edc.virtualized.dataplane.cert.store.CertStore;
import org.eclipse.edc.virtualized.dataplane.cert.store.sql.SqlCertStore;

import static org.eclipse.edc.virtualized.dataplane.cert.CertExchangeSqlExtension.NAME;


@Extension(NAME)
public class CertExchangeSqlExtension implements ServiceExtension {
public static final String NAME = "Cert Exchange Sql Store Extension";

@Setting(description = "The datasource to be used", defaultValue = DataSourceRegistry.DEFAULT_DATASOURCE, key = "edc.sql.store.certs.datasource")
private String dataSourceName;

@Inject
private DataSourceRegistry dataSourceRegistry;
@Inject
private TransactionContext transactionContext;
@Inject
private TypeManager typeManager;
@Inject
private QueryExecutor queryExecutor;

@Inject
private SqlSchemaBootstrapper sqlSchemaBootstrapper;

@Override
public void initialize(ServiceExtensionContext context) {
sqlSchemaBootstrapper.addStatementFromResource(dataSourceName, "certs-schema.sql");
}

@Provider
public CertStore certStore() {
return new SqlCertStore(dataSourceRegistry, dataSourceName, transactionContext, typeManager.getMapper(), queryExecutor);
}
}
Loading