Skip to content

Commit

Permalink
feat: add new V2 EDR API
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Mar 18, 2024
1 parent 1d5de46 commit 3a2c099
Show file tree
Hide file tree
Showing 35 changed files with 2,578 additions and 62 deletions.
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) {
return true;
}

var expiresIn = Long.parseLong(expiresInString);
// 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

0 comments on commit 3a2c099

Please sign in to comment.