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

feat: add new V2 EDR API #1140

Merged
Show file tree
Hide file tree
Changes from 6 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
101 changes: 101 additions & 0 deletions DEPENDENCIES

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions edc-extensions/edr/edr-api-v2/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/********************************************************************************
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/

plugins {
`java-library`
`maven-publish`
id("io.swagger.core.v3.swagger-gradle-plugin")
}

dependencies {
implementation(project(":spi:callback-spi"))
implementation(project(":spi:edr-spi"))
implementation(project(":spi:core-spi"))
implementation(project(":spi:tokenrefresh-spi"))

implementation(libs.edc.api.management)
implementation(libs.edc.core.validator)
implementation(libs.edc.spi.edrstore)
implementation(libs.jakarta.rsApi)

testImplementation(testFixtures(libs.edc.core.jersey))
testImplementation(libs.restAssured)
testImplementation(libs.edc.junit)
testImplementation(libs.edc.ext.jersey.providers)
testImplementation(libs.edc.core.transform)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.eclipse.tractusx.edc.api.edr.v2;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import org.eclipse.edc.api.model.ApiCoreSchema;
import org.eclipse.edc.connector.api.management.configuration.ManagementApiSchema;
import org.eclipse.edc.web.spi.ApiErrorDetail;
import org.eclipse.tractusx.edc.api.edr.v2.schema.EdrSchema;
import org.eclipse.tractusx.edc.edr.spi.types.EndpointDataReferenceEntry;

import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID;
import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE;

@OpenAPIDefinition
@Tag(name = "Control Plane EDR Api")
public interface EdrCacheApi {

@Operation(description = "Initiates an EDR negotiation by handling a contract negotiation first and then a transfer process for a given offer and with the given counter part. Please note that successfully invoking this endpoint " +
"only means that the negotiation was initiated.",
responses = {
@ApiResponse(responseCode = "200", description = "The negotiation was successfully initiated.",
content = @Content(schema = @Schema(implementation = ApiCoreSchema.IdResponseSchema.class))),
@ApiResponse(responseCode = "400", description = "Request body was malformed",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)))),
})
JsonObject initiateEdrNegotiation(@Schema(implementation = EdrSchema.NegotiateEdrRequestSchema.class) JsonObject dto);

@Operation(description = "Request all Edr entries according to a particular query",
requestBody = @RequestBody(
content = @Content(schema = @Schema(implementation = ApiCoreSchema.QuerySpecSchema.class))
),
responses = {
@ApiResponse(responseCode = "200", description = "The edr entries matching the query",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = EndpointDataReferenceEntrySchema.class)))),
@ApiResponse(responseCode = "400", description = "Request body was malformed",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class))))
})
JsonArray requestEdrEntries(JsonObject querySpecJson);

@Operation(description = "Gets the EDR data address with the given transfer process ID",
parameters = { @Parameter(name = "transferProcessId", description = "The ID of the transferprocess for which the EDR should be fetched", required = true),
@Parameter(name = "auto_refresh", description = "Whether the access token that is stored on the EDR should be checked for expiry, and renewed if necessary. Default is true")
},
responses = {
@ApiResponse(responseCode = "200", description = "The data address",
content = @Content(schema = @Schema(implementation = ManagementApiSchema.DataAddressSchema.class))),
@ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "404", description = "An EDR data address with the given transfer process ID does not exist",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class))))
}
)
JsonObject getEdrEntryDataAddress(String transferProcessId, boolean autoRefresh);

@Operation(description = "Removes an EDR entry given the transfer process ID",
responses = {
@ApiResponse(responseCode = "204", description = "EDR entry was deleted successfully"),
@ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "404", description = "An EDR entry with the given ID does not exist",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class))))
})
void removeEdrEntry(String transferProcessId);

@Operation(description = "Refreshes and returns the EDR data address with the given transfer process ID",
parameters = { @Parameter(name = "transferProcessId", description = "The ID of the transferprocess for which the EDR should be fetched", required = true),
},
responses = {
@ApiResponse(responseCode = "200", description = "The data address",
content = @Content(schema = @Schema(implementation = ManagementApiSchema.DataAddressSchema.class))),
@ApiResponse(responseCode = "400", description = "Request was malformed, e.g. id was null",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "404", description = "An EDR data address with the given transfer process ID does not exist",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiCoreSchema.ApiErrorDetailSchema.class))))
}
)
JsonObject refreshEdr(String transferProcessId);


@ArraySchema()
@Schema(name = "EndpointDataReferenceEntry", example = EndpointDataReferenceEntrySchema.EDR_ENTRY_OUTPUT_EXAMPLE)
record EndpointDataReferenceEntrySchema(
@Schema(name = ID)
String id,
@Schema(name = TYPE, example = EndpointDataReferenceEntry.EDR_ENTRY_TYPE)
String type
) {
public static final String EDR_ENTRY_OUTPUT_EXAMPLE = """
{
"@context": { "@vocab": "https://w3id.org/edc/v0.0.1/ns/" },
"@id": "transfer-process-id",
"transferProcessId": "transfer-process-id",
"agreementId": "agreement-id",
"contractNegotiationId": "contract-negotiation-id",
"assetId": "asset-id",
"providerId": "provider-id",
"createdAt": 1688465655
}
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package org.eclipse.tractusx.edc.api.edr.v2;

import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.edc.api.model.IdResponse;
import org.eclipse.edc.edr.spi.store.EndpointDataReferenceStore;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.spi.types.domain.DataAddress;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
import org.eclipse.edc.web.spi.exception.InvalidRequestException;
import org.eclipse.edc.web.spi.exception.ValidationFailureException;
import org.eclipse.tractusx.edc.api.edr.v2.dto.NegotiateEdrRequestDto;
import org.eclipse.tractusx.edc.edr.spi.service.EdrService;
import org.eclipse.tractusx.edc.edr.spi.types.EndpointDataReferenceEntry;
import org.eclipse.tractusx.edc.edr.spi.types.NegotiateEdrRequest;
import org.eclipse.tractusx.edc.spi.tokenrefresh.common.TokenRefreshHandler;

import java.time.Instant;

import static jakarta.json.stream.JsonCollectors.toJsonArray;
import static org.eclipse.edc.spi.query.QuerySpec.EDC_QUERY_SPEC_TYPE;
import static org.eclipse.edc.spi.result.ServiceResult.success;
import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper;
import static org.eclipse.tractusx.edc.edr.spi.CoreConstants.TX_AUTH_NS;

@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Path("/v2/edrs")
public class EdrCacheApiController implements EdrCacheApi {
private final EndpointDataReferenceStore edrStore;
private final TypeTransformerRegistry transformerRegistry;
private final JsonObjectValidatorRegistry validator;
private final Monitor monitor;
private final EdrService edrService;
private final TokenRefreshHandler tokenRefreshHandler;

public EdrCacheApiController(EndpointDataReferenceStore edrStore,
TypeTransformerRegistry transformerRegistry,
JsonObjectValidatorRegistry validator,
Monitor monitor,
EdrService edrService,
TokenRefreshHandler tokenRefreshHandler) {
this.edrStore = edrStore;
this.transformerRegistry = transformerRegistry;
this.validator = validator;
this.monitor = monitor;
this.edrService = edrService;
this.tokenRefreshHandler = tokenRefreshHandler;
}

@POST
@Override
public JsonObject initiateEdrNegotiation(JsonObject requestObject) {
validator.validate(NegotiateEdrRequestDto.EDR_REQUEST_DTO_TYPE, requestObject).orElseThrow(ValidationFailureException::new);

var edrNegotiationRequest = transformerRegistry.transform(requestObject, NegotiateEdrRequestDto.class)
.compose(dto -> transformerRegistry.transform(dto, NegotiateEdrRequest.class))
.orElseThrow(InvalidRequestException::new);

var contractNegotiation = edrService.initiateEdrNegotiation(edrNegotiationRequest).orElseThrow(exceptionMapper(NegotiateEdrRequest.class));

var idResponse = IdResponse.Builder.newInstance()
.id(contractNegotiation.getId())
.createdAt(contractNegotiation.getCreatedAt())
.build();

return transformerRegistry.transform(idResponse, JsonObject.class)
.orElseThrow(f -> new EdcException("Error creating response body: " + f.getFailureDetail()));
}

@POST
@Path("/request")
@Override
public JsonArray requestEdrEntries(JsonObject querySpecJson) {
QuerySpec querySpec;
if (querySpecJson == null) {
querySpec = QuerySpec.Builder.newInstance().build();
} else {
validator.validate(EDC_QUERY_SPEC_TYPE, querySpecJson).orElseThrow(ValidationFailureException::new);

querySpec = transformerRegistry.transform(querySpecJson, QuerySpec.class)
.orElseThrow(InvalidRequestException::new);
}

return edrStore.query(querySpec)
.flatMap(ServiceResult::from)
.orElseThrow(exceptionMapper(QuerySpec.class, null)).stream()
.map(it -> transformerRegistry.transform(it, JsonObject.class))
.peek(r -> r.onFailure(f -> monitor.warning(f.getFailureDetail())))
.filter(Result::succeeded)
.map(Result::getContent)
.collect(toJsonArray());
}

@GET
@Path("{transferProcessId}/dataaddress")
@Override
public JsonObject getEdrEntryDataAddress(@PathParam("transferProcessId") String transferProcessId, @QueryParam("auto_refresh") boolean autoRefresh) {

var dataAddress = edrStore.resolveByTransferProcess(transferProcessId)
.flatMap(ServiceResult::from)
.compose(edr -> autoRefresh ? refreshAndUpdateToken(edr, transferProcessId) : success(edr))
.orElseThrow(exceptionMapper(EndpointDataReferenceEntry.class, transferProcessId));

return transformerRegistry.transform(dataAddress, JsonObject.class)
.orElseThrow(f -> new EdcException(f.getFailureDetail()));


}

@DELETE
@Path("{transferProcessId}")
@Override
public void removeEdrEntry(@PathParam("transferProcessId") String transferProcessId) {
edrStore.delete(transferProcessId)
.flatMap(ServiceResult::from)
.orElseThrow(exceptionMapper(EndpointDataReferenceEntry.class, transferProcessId));
}

@POST
@Path("{transferProcessId}/refresh")
@Override
public JsonObject refreshEdr(@PathParam("transferProcessId") String transferProcessId) {
var updatedEdr = tokenRefreshHandler.refreshToken(transferProcessId)
.orElseThrow(exceptionMapper(DataAddress.class, transferProcessId));

return transformerRegistry.transform(updatedEdr, JsonObject.class)
.orElseThrow(f -> new EdcException(f.getFailureDetail()));
}

// todo: move this method into a service once the "old" EDR api,service,etc. is removed
private ServiceResult<DataAddress> refreshAndUpdateToken(DataAddress edr, String id) {

var edrEntry = edrStore.findById(id);
if (edrEntry == null) {
return ServiceResult.notFound("An EndpointDataReferenceEntry with ID '%s' does not exist".formatted(id));
}

if (isExpired(edr, edrEntry)) {
monitor.debug("Token expired, need to refresh.");
return tokenRefreshHandler.refreshToken(id, edr);
}
return ServiceResult.success(edr);
}

// todo: move this method into a service once the "old" EDR api,service,etc. is removed
private boolean isExpired(DataAddress edr, org.eclipse.edc.edr.spi.types.EndpointDataReferenceEntry metadata) {
var expiresInString = edr.getStringProperty(TX_AUTH_NS + "expiresIn");
if (expiresInString == null) {
paullatzelsperger marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

var expiresIn = Long.parseLong(expiresInString);

Check notice

Code scanning / CodeQL

Missing catch of NumberFormatException Note

Potential uncaught 'java.lang.NumberFormatException'.
// createdAt is in millis, expires-in is in seconds
var expiresAt = metadata.getCreatedAt() / 1000L + expiresIn;
var expiresAtInstant = Instant.ofEpochSecond(expiresAt);

return expiresAtInstant.isBefore(Instant.now());
}

}
Loading
Loading