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

feature: Agreement Retirement #1601

Closed
2 changes: 2 additions & 0 deletions edc-controlplane/edc-controlplane-base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ dependencies {
runtimeOnly(project(":edc-extensions:edr:edr-api-v2"))
runtimeOnly(project(":edc-extensions:edr:edr-callback"))
runtimeOnly(project(":edc-extensions:tokenrefresh-handler"))
runtimeOnly(project(":edc-extensions:agreements"))

runtimeOnly(libs.edc.core.edrstore)
runtimeOnly(libs.edc.edr.store.receiver)
runtimeOnly(libs.edc.dpf.transfer.signaling)
Expand Down
38 changes: 38 additions & 0 deletions edc-extensions/agreements/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Agreements Retirement Extension

This extension is introduced to allow a dataspace dataset provider to _prematurely_ retire an active contract agreement.

Contract agreements are immutable entities by design.
The word prematurely is used here since contract agreements should only expire once the contractual terms agreed upon between participants no longer holds.

Even though the previous statements are valid, the need to prematurely retire an active contract agreement exists if, for example,
the contract agreement is a digital representation of a physical agreement which might have changed via legal
mechanisms, hence resulting in a new contract. The digital representation is no longer valid and shouldn't allow any data transfers.

## Technical approach

Since contract agreements are immutable, the following approach was used to represent a contract agreement retirement.

A policy function was introduced that checks if the attached contract agreement exists in the `AgreementRetirementStore`.
If true, the evaluation is considered failed.

This policy function is registered both in the `policy-monitor` and `transfer` scopes. In both cases, a failed policy evaluation
leads to transfer process termination.

An API was created to enable dataset providers to manage `AgreementRetirementEntry` entities in the `AgreementRetirementStore` via an endpoint.

## AgreementRetirementEntry schema

An `AgreementRetirementEntry` is composed of:
- A contract agreement id.
- A retirement reason.
- An agreement retirement timestamp.

`AgreementRetirementEntry` entities can be managed via the `/retireagreements` endpoint of the data management API.
Please refer to the swagger API spec for detailed examples on how to manage these entities.

## Impact on active or new transfer processes

Once a contract agreement is retired, all active transfer processes related with that agreement
will be terminated. New transfer process requests made from the consumer using the retired agreement will also fail with an
agreement is invalid message.
29 changes: 29 additions & 0 deletions edc-extensions/agreements/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/********************************************************************************
* Copyright (c) 2024 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`
}

dependencies {
api(project(":edc-extensions:agreements:retirement-evaluation-core"))
api(project(":edc-extensions:agreements:retirement-evaluation-api"))
api(project(":edc-extensions:agreements:retirement-evaluation-spi"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/********************************************************************************
* Copyright (c) 2024 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(libs.plugins.swagger.get().pluginId)
}
dependencies {

implementation(project(":edc-extensions:agreements:retirement-evaluation-spi"))
implementation(libs.edc.runtime.metamodel)
implementation(libs.edc.api.management)

implementation(libs.jakarta.rsApi)

testImplementation(testFixtures(libs.edc.core.jersey))
testImplementation(libs.edc.spi.core)
testImplementation(libs.edc.junit)
testImplementation(libs.restAssured)
testImplementation(project(":edc-extensions:agreements:retirement-evaluation-spi"))
}

edcBuild {
swagger {
apiGroup.set("control-plane")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/********************************************************************************
* Copyright (c) 2024 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
********************************************************************************/

package org.eclipse.tractusx.edc.agreements.retirement.api;

import jakarta.json.Json;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
import org.eclipse.edc.web.spi.WebService;
import org.eclipse.edc.web.spi.configuration.ApiContext;
import org.eclipse.tractusx.edc.agreements.retirement.api.transform.JsonObjectFromAgreementRetirementTransformer;
import org.eclipse.tractusx.edc.agreements.retirement.api.transform.JsonObjectToAgreementsRetirementEntryTransformer;
import org.eclipse.tractusx.edc.agreements.retirement.api.v3.AgreementsRetirementApiV3Controller;
import org.eclipse.tractusx.edc.agreements.retirement.spi.store.AgreementsRetirementStore;

import java.util.Map;

import static org.eclipse.tractusx.edc.agreements.retirement.api.AgreementsRetirementApiExtension.NAME;


@Extension(value = NAME)
public class AgreementsRetirementApiExtension implements ServiceExtension {

public static final String NAME = "Agreements Retirement API Extension ";

@Override
public String name() {
return NAME;
}

@Inject
private WebService webService;
@Inject
private TypeTransformerRegistry transformerRegistry;
@Inject
private JsonObjectValidatorRegistry validator;
@Inject
private AgreementsRetirementStore store;
@Inject
private Monitor monitor;

@Override
public void initialize(ServiceExtensionContext context) {
var jsonFactory = Json.createBuilderFactory(Map.of());
var managementTypeTransformerRegistry = transformerRegistry.forContext("management-api");

managementTypeTransformerRegistry.register(new JsonObjectFromAgreementRetirementTransformer(jsonFactory));
managementTypeTransformerRegistry.register(new JsonObjectToAgreementsRetirementEntryTransformer());

webService.registerResource(ApiContext.MANAGEMENT, new AgreementsRetirementApiV3Controller(store, managementTypeTransformerRegistry, validator, monitor));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/********************************************************************************
* Copyright (c) 2024 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
********************************************************************************/

package org.eclipse.tractusx.edc.agreements.retirement.api.transform;

import jakarta.json.JsonBuilderFactory;
import jakarta.json.JsonObject;
import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer;
import org.eclipse.edc.transform.spi.TransformerContext;
import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class JsonObjectFromAgreementRetirementTransformer extends AbstractJsonLdTransformer<AgreementsRetirementEntry, JsonObject> {

JsonBuilderFactory jsonFactory;

public JsonObjectFromAgreementRetirementTransformer(JsonBuilderFactory jsonFactory) {
super(AgreementsRetirementEntry.class, JsonObject.class);
this.jsonFactory = jsonFactory;
}


@Override
public @Nullable JsonObject transform(@NotNull AgreementsRetirementEntry entry, @NotNull TransformerContext transformerContext) {
return jsonFactory.createObjectBuilder()
.add(AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID, entry.getAgreementId())
.add(AgreementsRetirementEntry.AR_ENTRY_REASON, entry.getReason())
.add(AgreementsRetirementEntry.AR_ENTRY_RETIREMENT_DATE, entry.getAgreementRetirementDate())
.build();

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/********************************************************************************
* Copyright (c) 2024 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
********************************************************************************/

package org.eclipse.tractusx.edc.agreements.retirement.api.transform;

import jakarta.json.JsonObject;
import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer;
import org.eclipse.edc.transform.spi.TransformerContext;
import org.eclipse.tractusx.edc.agreements.retirement.spi.types.AgreementsRetirementEntry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class JsonObjectToAgreementsRetirementEntryTransformer extends AbstractJsonLdTransformer<JsonObject, AgreementsRetirementEntry> {

public JsonObjectToAgreementsRetirementEntryTransformer() {
super(JsonObject.class, AgreementsRetirementEntry.class);
}

@Override
public @Nullable AgreementsRetirementEntry transform(@NotNull JsonObject jsonObject, @NotNull TransformerContext context) {
var entryBuilder = AgreementsRetirementEntry.Builder.newInstance();
entryBuilder.withAgreementId(transformString(jsonObject.get(AgreementsRetirementEntry.AR_ENTRY_AGREEMENT_ID), context));
entryBuilder.withReason(transformString(jsonObject.get(AgreementsRetirementEntry.AR_ENTRY_REASON), context));
if (jsonObject.containsKey(AgreementsRetirementEntry.AR_ENTRY_RETIREMENT_DATE)) {
entryBuilder.withAgreementRetirementDate(transformString(jsonObject.get(AgreementsRetirementEntry.AR_ENTRY_RETIREMENT_DATE), context));
}
return entryBuilder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/********************************************************************************
* Copyright (c) 2024 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
********************************************************************************/

package org.eclipse.tractusx.edc.agreements.retirement.api.v3;

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.info.Info;
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.web.spi.ApiErrorDetail;

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

@OpenAPIDefinition(info = @Info(description = "With this API clients can retire an active Contract Agreement. Clients can also list all retired agreements.", title = "Agreements Retirement API"))
@Tag(name = "Agreements Retirement")
public interface AgreementsRetirementApiV3 {


@Operation(description = "Get all retired contract agreements.",
responses = {
@ApiResponse(responseCode = "200", description = "A list of retired contract agreements"),
@ApiResponse(responseCode = "400", description = "Request body was malformed",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class))))
})
JsonArray getAllRetiredV3(JsonObject querySpecJson);

@Operation(description = "Removes a contract agreement from the retired list, reactivating it.",
responses = {
@ApiResponse(responseCode = "204", description = "The contract agreement is reactivated"),
@ApiResponse(responseCode = "404", description = "No entry for the given agreementId was found"),
@ApiResponse(responseCode = "400", description = "Request body was malformed",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class))))
})
void reactivateRetiredV3(@Parameter(name = "agreementId", description = "The contract agreement id") String agreementId);

@Operation(description = "Retires an active contract agreement.",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = RetirementSchema.class))),

responses = {
@ApiResponse(responseCode = "204", description = "The contract agreement was successfully retired"),
@ApiResponse(responseCode = "409", description = "The contract agreement is already retired"),
@ApiResponse(responseCode = "400", description = "Request body was malformed",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class))))
})
void retireAgreementV3(JsonObject entry);


@Schema(name = "Retirement Example", example = RetirementSchema.EXAMPLE)
record RetirementSchema(
@Schema(name = ID) String id,
String reason
) {
public static final String EXAMPLE = """
{
"@context": {
"tx": "https://w3id.org/tractusx/v0.0.1/ns/"
},
"@id": "contract-agreement-id",
"tx:reason": "This contract agreement was retired since the physical counterpart is no longer valid."
}
""";
}

}
Loading
Loading