Skip to content
Open
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
18 changes: 15 additions & 3 deletions extensions/data-plane-certs/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,26 @@ dependencies {
api(libs.edc.spi.http)
api(libs.edc.spi.transaction)
api(libs.edc.spi.web)
api(libs.edc.spi.dataplane)
implementation(libs.edc.core.boot)
implementation(libs.edc.core.runtime)
implementation(libs.edc.core.token)
implementation(libs.edc.core.connector)
implementation(libs.edc.core.api)
implementation(libs.edc.core.participantcontext.config)
implementation(libs.jersey.multipart)
implementation(libs.edc.lib.util)
implementation(libs.edc.lib.sql)
implementation(libs.edc.lib.token)
implementation(libs.edc.lib.oauth2.authn)
implementation(libs.edc.core.sql.bootstrapper)
implementation(libs.edc.lib.util.dataplane)
implementation(libs.edc.dataplane.iam)
implementation(libs.edc.core.sql)
implementation(libs.edc.core.http)
implementation(libs.edc.transaction.local)
implementation(libs.edc.transaction.pool)
implementation(libs.edc.api.control.configuration)
implementation(libs.edc.api.observability)
implementation(libs.jakarta.rsApi)
implementation(libs.postgres)

testImplementation(libs.edc.lib.http)
testImplementation(libs.edc.junit)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@

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.api.authentication.JwksResolver;
import org.eclipse.edc.api.authentication.filter.JwtValidatorFilter;
import org.eclipse.edc.keys.spi.KeyParserRegistry;
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.EdcException;
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.token.rules.ExpirationIssuedAtValidationRule;
import org.eclipse.edc.token.rules.IssuerEqualsValidationRule;
import org.eclipse.edc.token.rules.NotBeforeValidationRule;
import org.eclipse.edc.token.spi.TokenValidationRule;
import org.eclipse.edc.token.spi.TokenValidationService;
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;
Expand All @@ -35,6 +39,11 @@
import org.eclipse.edc.web.spi.configuration.PortMapping;
import org.eclipse.edc.web.spi.configuration.PortMappingRegistry;

import java.net.MalformedURLException;
import java.net.URL;
import java.time.Clock;
import java.util.List;

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

@Extension(NAME)
Expand All @@ -43,12 +52,7 @@ public class CertExchangeExtension implements ServiceExtension {
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;
private static final long FIVE_MINUTES = 1000 * 60 * 5;

@Configuration
private CertApiConfiguration apiConfiguration;
Expand All @@ -60,41 +64,54 @@ public class CertExchangeExtension implements ServiceExtension {
private PortMappingRegistry portMappingRegistry;

@Inject
private DataPlaneAuthorizationService authorizationService;
private WebService webService;

@Inject
private PublicEndpointGeneratorService generatorService;
private CertStore certStore;

@Inject
private EndpointDataReferenceServiceRegistry endpointDataReferenceServiceRegistry;
private TransactionContext transactionContext;

@Inject
private WebService webService;
private TokenValidationService tokenValidationService;

@Configuration
private SigletConfig sigletConfig;

@Inject
private CertStore certStore;
private KeyParserRegistry keyParserRegistry;

@Inject
private TransactionContext transactionContext;
private Clock clock;

@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));
URL url;
try {
url = new URL(sigletConfig.jwksUrl());
} catch (MalformedURLException e) {
throw new EdcException(e);
}
var endpoint = Endpoint.url(publicBaseUrl);
generatorService.addGeneratorFunction("HttpCertData", dataAddress -> endpoint);
webService.registerResource(API_CONTEXT, new CertExchangePublicController(authorizationService, certStore, transactionContext));

webService.registerResource(API_CONTEXT, new CertExchangePublicController(certStore, transactionContext));
webService.registerResource(API_CONTEXT, new JwtValidatorFilter(tokenValidationService, new JwksResolver(url, keyParserRegistry, sigletConfig.cacheValidityInMillis), getRules()));

webService.registerResource("control", new CertInternalExchangeController(certStore, transactionContext));

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

private List<TokenValidationRule> getRules() {
return List.of(
new IssuerEqualsValidationRule(sigletConfig.expectedIssuer),
new NotBeforeValidationRule(clock, 0, true),
new ExpirationIssuedAtValidationRule(clock, 0, false)
);
}


@Settings
record CertApiConfiguration(
@Setting(key = "web.http." + API_CONTEXT + ".port", description = "Port for " + API_CONTEXT + " api context", defaultValue = DEFAULT_CERTS_PORT + "")
Expand All @@ -104,4 +121,16 @@ record CertApiConfiguration(
) {

}

@Settings
record SigletConfig(
@Setting(key = "edc.iam.siglet.issuer", description = "Issuer of the Siglet server", required = false)
String expectedIssuer,
@Setting(key = "edc.iam.siglet.jwks.url", description = "Absolute URL where the JWKS of the Siglet server is hosted")
String jwksUrl,
@Setting(key = "edc.iam.siglet.jwks.cache.validity", description = "Time (in ms) that cached JWKS are cached", defaultValue = "" + FIVE_MINUTES)
long cacheValidityInMillis
) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
package org.eclipse.edc.virtualized.dataplane.cert.api;

import com.fasterxml.jackson.core.type.TypeReference;
import com.nimbusds.jwt.SignedJWT;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.StreamingOutput;
import org.eclipse.edc.connector.dataplane.spi.iam.DataPlaneAuthorizationService;
import org.eclipse.edc.spi.iam.ClaimToken;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.transaction.spi.TransactionContext;
import org.eclipse.edc.virtualized.dataplane.cert.model.ActivityItem;
Expand All @@ -35,30 +35,27 @@
import java.io.InputStream;
import java.time.Instant;
import java.util.List;
import java.util.Map;

import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static jakarta.ws.rs.core.Response.Status.FORBIDDEN;
import static jakarta.ws.rs.core.Response.Status.UNAUTHORIZED;
import static org.eclipse.edc.api.authentication.filter.Constants.REQUEST_PROPERTY_CLAIMS;

@Path("certs")
public class CertExchangePublicController {

private final DataPlaneAuthorizationService authorizationService;
private final CertStore certStore;
private final TransactionContext transactionContext;

public CertExchangePublicController(DataPlaneAuthorizationService authorizationService, CertStore certStore, TransactionContext transactionContext) {
this.authorizationService = authorizationService;
public CertExchangePublicController(CertStore certStore, TransactionContext transactionContext) {
this.certStore = certStore;
this.transactionContext = transactionContext;
}

@POST
@Path("/request")
public List<CertMetadata> queryCertificates(@HeaderParam(AUTHORIZATION) String token, QuerySpec querySpec) {
public List<CertMetadata> queryCertificates(@Context ContainerRequestContext requestContext, QuerySpec querySpec) {
return transactionContext.execute(() -> {
checkAuth(token);
checkAuth(requestContext);
return certStore.queryMetadata(querySpec)
// strip out the history for public API
.stream().map(ct -> new CertMetadata(ct.id(), ct.contentType(), ct.properties())).toList();
Expand All @@ -67,54 +64,46 @@ public List<CertMetadata> queryCertificates(@HeaderParam(AUTHORIZATION) String t

@GET
@Path("/{id}")
public Response certificateDownload(@HeaderParam(AUTHORIZATION) String token, @PathParam("id") String id) {
public Response certificateDownload(@Context ContainerRequestContext requestContext, @PathParam("id") String id) {
return transactionContext.execute(() -> {
var subject = checkAuth(token);
var claims = checkAuth(requestContext);
var metadata = certStore.getMetadata(id);
if (metadata == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
metadata.history().add(new ActivityItem(subject, Instant.now().getEpochSecond(), "DOWNLOAD"));
certStore.updateMetadata(id, metadata);
StreamingOutput stream = output -> {
try (InputStream is = certStore.retrieve(id)) {
is.transferTo(output);
}
};

var subject = claims.getClaim("sub");
if (subject instanceof String s) {
metadata.history().add(new ActivityItem(s, Instant.now().getEpochSecond(), "DOWNLOAD"));
certStore.updateMetadata(id, metadata);
StreamingOutput stream = output -> {
try (InputStream is = certStore.retrieve(id)) {
is.transferTo(output);
}
};


return Response.ok(stream)
.header("Content-Type", metadata.contentType())
.build();
} else {
throw new WebApplicationException(FORBIDDEN);
}

return Response.ok(stream)
.header("Content-Type", metadata.contentType())
.build();
});
}

private String checkAuth(String token) {
if (token == null) {
private ClaimToken checkAuth(ContainerRequestContext requestContext) {
if (requestContext == null) {
throw new WebApplicationException(UNAUTHORIZED);
}

// assuming "token" is a JWT, lets parse it and extract the `sub` claim
var subject = parseJwt(token);


var sourceDataAddress = authorizationService.authorize(token, Map.of());
if (sourceDataAddress.failed()) {
var claims = requestContext.getProperty(REQUEST_PROPERTY_CLAIMS);
if (claims instanceof ClaimToken claimToken) {
return claimToken;
} else {
throw new WebApplicationException(FORBIDDEN);

}

return subject;
}

private String parseJwt(String token) {
try {
var signedJwt = SignedJWT.parse(token);
return String.join(",", signedJwt.getJWTClaimsSet().getAudience());
} catch (Exception e) {
return null;
}
}

@NotNull
protected <T> TypeReference<T> getTypeRef() {
Expand Down
47 changes: 0 additions & 47 deletions extensions/data-plane-public-api-v2/build.gradle.kts

This file was deleted.

Loading
Loading