From 7df483478fcdd3a612531277d9a7c9c1d70de145 Mon Sep 17 00:00:00 2001 From: ndr_brt Date: Fri, 29 May 2026 14:30:23 +0200 Subject: [PATCH] fix: add missing breakLease calls in ContractNegotiationProtocolServiceImpl --- ...ontractNegotiationProtocolServiceImpl.java | 251 ++++----- .../protocol/ProtocolTokenValidatorImpl.java | 6 +- ...actNegotiationProtocolServiceImplTest.java | 521 +++++++++--------- .../offer/ConsumerOfferResolverImpl.java | 48 +- .../ContractValidationServiceImpl.java | 64 ++- .../offer/ConsumerOfferResolverImplTest.java | 4 +- .../ContractValidationServiceImplTest.java | 7 - .../validation/ContractValidationService.java | 4 +- .../validation/ValidatableConsumerOffer.java | 4 + .../validation/ValidatedConsumerOffer.java | 44 -- 10 files changed, 461 insertions(+), 492 deletions(-) delete mode 100644 spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatedConsumerOffer.java diff --git a/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImpl.java b/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImpl.java index f99e6ef3f25..ca31f030be3 100644 --- a/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImpl.java +++ b/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImpl.java @@ -29,16 +29,17 @@ import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractNegotiationTerminationMessage; import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractOfferMessage; import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequestMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractOffer; import org.eclipse.edc.connector.controlplane.contract.spi.types.protocol.ContractRemoteMessage; import org.eclipse.edc.connector.controlplane.contract.spi.validation.ContractValidationService; import org.eclipse.edc.connector.controlplane.contract.spi.validation.ValidatableConsumerOffer; -import org.eclipse.edc.connector.controlplane.contract.spi.validation.ValidatedConsumerOffer; import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationProtocolService; import org.eclipse.edc.connector.controlplane.services.spi.protocol.ProtocolTokenValidator; import org.eclipse.edc.participant.spi.ParticipantAgent; import org.eclipse.edc.participantcontext.spi.types.ParticipantContext; import org.eclipse.edc.policy.context.request.spi.RequestContractNegotiationPolicyContext; import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.PolicyType; import org.eclipse.edc.spi.iam.TokenRepresentation; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.result.ServiceResult; @@ -90,28 +91,27 @@ public ContractNegotiationProtocolServiceImpl(ContractNegotiationStore store, public ServiceResult notifyRequested(ParticipantContext participantContext, ContractRequestMessage message, TokenRepresentation tokenRepresentation) { return transactionContext.execute(() -> fetchValidatableOffer(participantContext, message) .compose(validatableOffer -> verifyRequest(participantContext, tokenRepresentation, validatableOffer.getContractPolicy(), message) - .compose(agent -> validateOffer(agent, validatableOffer)) - .compose(validatedOffer -> { - var result = message.getProviderPid() == null - ? createNegotiation(participantContext, message, validatedOffer.getConsumerIdentity(), PROVIDER, message.getCallbackAddress()) - : getAndLeaseNegotiation(participantContext, message.getProviderPid()); - - return result.compose(negotiation -> { - if (negotiation.shouldIgnoreIncomingMessage(message.getId())) { - return ServiceResult.success(negotiation); - } - if (negotiation.getType().equals(PROVIDER) && negotiation.canBeRequestedProvider()) { - negotiation.protocolMessageReceived(message.getId()); - negotiation.addContractOffer(validatedOffer.getOffer()); - negotiation.transitionRequested(); - update(negotiation); - observable.invokeForEach(l -> l.requested(negotiation)); - return ServiceResult.success(negotiation); - } else { - return ServiceResult.conflict(format("Cannot process %s because %s", message.getClass().getSimpleName(), "negotiation cannot be requested")); - } - - }); + .compose(agent -> { + var result = validationService.validateInitialOffer(agent, validatableOffer); + if (result.failed()) { + monitor.debug("[Provider] Contract offer rejected as invalid: " + result.getFailureDetail()); + return ServiceResult.badRequest("Contract offer is not valid: " + result.getFailureDetail()); + } + + var offerId = validatableOffer.getOfferId(); + var contractOffer = ContractOffer.Builder.newInstance() + .id(offerId.toString()) + .policy(validatableOffer.getTargetedContractPolicy().toBuilder().type(PolicyType.OFFER).build()) + .assetId(offerId.assetIdPart()) + .build(); + + if (message.getProviderPid() == null) { + var negotiation = createNegotiation(participantContext, message, agent.getIdentity(), PROVIDER, message.getCallbackAddress()); + return requestedAction(message, negotiation, contractOffer); + } + + return onMessageDo(participantContext, message, agent, + negotiation -> requestedAction(message, negotiation, contractOffer)); }) )); } @@ -122,27 +122,13 @@ public ServiceResult notifyRequested(ParticipantContext par public ServiceResult notifyOffered(ParticipantContext participantContext, ContractOfferMessage message, TokenRepresentation tokenRepresentation) { return transactionContext.execute(() -> verifyRequest(participantContext, tokenRepresentation, message.getContractOffer().getPolicy(), message) .compose(agent -> { - ServiceResult result = message.getConsumerPid() == null - ? createNegotiation(participantContext, message, agent.getIdentity(), CONSUMER, message.getCallbackAddress()) - : getAndLeaseNegotiation(participantContext, message.getConsumerPid()) - .compose(negotiation -> validateRequest(agent, negotiation).map(it -> negotiation)); - - return result.compose(negotiation -> { - if (negotiation.shouldIgnoreIncomingMessage(message.getId())) { - return ServiceResult.success(negotiation); - } - if (negotiation.getType().equals(CONSUMER) && negotiation.canBeOfferedConsumer()) { - negotiation.protocolMessageReceived(message.getId()); - negotiation.addContractOffer(message.getContractOffer()); - negotiation.transitionOffered(); - update(negotiation); - observable.invokeForEach(l -> l.offered(negotiation)); - - return ServiceResult.success(negotiation); - } else { - return ServiceResult.conflict(format("Cannot process %s because %s", message.getClass().getSimpleName(), "negotiation cannot be offered")); - } - }); + if (message.getConsumerPid() == null) { + var negotiation = createNegotiation(participantContext, message, agent.getIdentity(), CONSUMER, message.getCallbackAddress()); + return offeredAction(message, negotiation); + } + + return onMessageDo(participantContext, message, agent, + negotiation -> offeredAction(message, negotiation)); })); } @@ -151,9 +137,8 @@ public ServiceResult notifyOffered(ParticipantContext parti @NotNull public ServiceResult notifyAccepted(ParticipantContext participantContext, ContractNegotiationEventMessage message, TokenRepresentation tokenRepresentation) { return transactionContext.execute(() -> getNegotiation(participantContext, message.getProcessId()) - .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message) - .compose(agent -> validateRequest(agent, contractNegotiation))) - .compose(cn -> onMessageDo(participantContext, message, contractNegotiation -> acceptedAction(message, contractNegotiation)))); + .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message)) + .compose(agent -> onMessageDo(participantContext, message, agent, contractNegotiation -> acceptedAction(message, contractNegotiation)))); } @@ -162,10 +147,8 @@ public ServiceResult notifyAccepted(ParticipantContext part @NotNull public ServiceResult notifyAgreed(ParticipantContext participantContext, ContractAgreementMessage message, TokenRepresentation tokenRepresentation) { return transactionContext.execute(() -> getNegotiation(participantContext, message.getProcessId()) - .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message) - .compose(agent -> validateAgreed(message, agent, contractNegotiation)) - ) - .compose(agent -> onMessageDo(participantContext, message, negotiation -> agreedAction(message, negotiation, agent)))); + .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message)) + .compose(agent -> onMessageDo(participantContext, message, agent, negotiation -> agreedAction(message, negotiation, agent)))); } @Override @@ -173,9 +156,8 @@ public ServiceResult notifyAgreed(ParticipantContext partic @NotNull public ServiceResult notifyVerified(ParticipantContext participantContext, ContractAgreementVerificationMessage message, TokenRepresentation tokenRepresentation) { return transactionContext.execute(() -> getNegotiation(participantContext, message.getProcessId()) - .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message) - .compose(agent -> validateRequest(agent, contractNegotiation))) - .compose(agent -> onMessageDo(participantContext, message, contractNegotiation -> verifiedAction(message, contractNegotiation, agent)))); + .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message)) + .compose(agent -> onMessageDo(participantContext, message, agent, contractNegotiation -> verifiedAction(message, contractNegotiation, agent)))); } @Override @@ -183,9 +165,8 @@ public ServiceResult notifyVerified(ParticipantContext part @NotNull public ServiceResult notifyFinalized(ParticipantContext participantContext, ContractNegotiationEventMessage message, TokenRepresentation tokenRepresentation) { return transactionContext.execute(() -> getNegotiation(participantContext, message.getProcessId()) - .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message) - .compose(agent -> validateRequest(agent, contractNegotiation))) - .compose(cn -> onMessageDo(participantContext, message, contractNegotiation -> finalizedAction(message, contractNegotiation)))); + .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message)) + .compose(agent -> onMessageDo(participantContext, message, agent, contractNegotiation -> finalizedAction(message, contractNegotiation)))); } @Override @@ -193,9 +174,8 @@ public ServiceResult notifyFinalized(ParticipantContext par @NotNull public ServiceResult notifyTerminated(ParticipantContext participantContext, ContractNegotiationTerminationMessage message, TokenRepresentation tokenRepresentation) { return transactionContext.execute(() -> getNegotiation(participantContext, message.getProcessId()) - .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message) - .compose(agent -> validateRequest(agent, contractNegotiation))) - .compose(cn -> onMessageDo(participantContext, message, contractNegotiation -> terminatedAction(message, contractNegotiation)))); + .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message)) + .compose(agent -> onMessageDo(participantContext, message, agent, contractNegotiation -> terminatedAction(message, contractNegotiation)))); } @Override @@ -205,26 +185,48 @@ public ServiceResult findById(ParticipantContext participan return transactionContext.execute(() -> getNegotiation(participantContext, message.getNegotiationId()) .compose(contractNegotiation -> verifyRequest(participantContext, tokenRepresentation, contractNegotiation.getLastContractOffer().getPolicy(), message) - .compose(agent -> validateRequest(agent, contractNegotiation) - .map(it -> contractNegotiation)))); + .compose(agent -> { + var result = validationService.validateRequest(agent, contractNegotiation); + if (result.failed()) { + return ServiceResult.badRequest("Invalid client credentials: " + result.getFailureDetail()); + } + + return ServiceResult.success(contractNegotiation); + }))); } @NotNull - private ServiceResult onMessageDo(ParticipantContext participantContext, ContractRemoteMessage message, Function> action) { - return getAndLeaseNegotiation(participantContext, message.getProcessId()) - .compose(contractNegotiation -> { - if (contractNegotiation.shouldIgnoreIncomingMessage(message.getId())) { - return ServiceResult.success(contractNegotiation); - } else { - return action.apply(contractNegotiation) - .onFailure(f -> breakLease(contractNegotiation)); - } - }); + private ServiceResult onMessageDo(ParticipantContext participantContext, ContractRemoteMessage message, + ParticipantAgent agent, Function> action) { + var leaseNegotiation = store.findByIdAndLease(message.getProcessId()); + if (leaseNegotiation.failed()) { + return ServiceResult.from(leaseNegotiation.mapFailure()); + } + + var negotiation = leaseNegotiation.getContent(); + if (!negotiation.getParticipantContextId().equals(participantContext.getParticipantContextId())) { + store.breakLease(negotiation); + return ServiceResult.notFound("No negotiation with id %s found".formatted(negotiation.getId())); + } + + var result = validationService.validateRequest(agent, negotiation); + if (result.failed()) { + store.breakLease(negotiation); + return ServiceResult.badRequest("Invalid client credentials: " + result.getFailureDetail()); + } + + if (negotiation.shouldIgnoreIncomingMessage(message.getId())) { + store.breakLease(negotiation); + return ServiceResult.success(negotiation); + } + + return action.apply(negotiation) + .onFailure(f -> store.breakLease(negotiation)); } @NotNull - private ServiceResult createNegotiation(ParticipantContext participantContext, ContractRemoteMessage message, String counterPartyIdentity, ContractNegotiation.Type type, String callbackAddress) { - var negotiation = ContractNegotiation.Builder.newInstance() + private ContractNegotiation createNegotiation(ParticipantContext participantContext, ContractRemoteMessage message, String counterPartyIdentity, ContractNegotiation.Type type, String callbackAddress) { + return ContractNegotiation.Builder.newInstance() .id(UUID.randomUUID().toString()) .correlationId(message.getConsumerPid()) .counterPartyId(counterPartyIdentity) @@ -234,19 +236,6 @@ private ServiceResult createNegotiation(ParticipantContext .type(type) .participantContextId(participantContext.getParticipantContextId()) .build(); - - return ServiceResult.success(negotiation); - } - - @NotNull - private ServiceResult validateOffer(ParticipantAgent agent, ValidatableConsumerOffer consumerOffer) { - var result = validationService.validateInitialOffer(agent, consumerOffer); - if (result.failed()) { - monitor.debug("[Provider] Contract offer rejected as invalid: " + result.getFailureDetail()); - return ServiceResult.badRequest("Contract offer is not valid: " + result.getFailureDetail()); - } else { - return ServiceResult.success(result.getContent()); - } } @NotNull @@ -254,39 +243,44 @@ private ServiceResult fetchValidatableOffer(Participan var offerId = message.getContractOffer().getId(); var result = consumerOfferResolver.resolveOffer(offerId); + + if (result.succeeded() && participantContext.getParticipantContextId().equals(result.getContent().getContractDefinition().getParticipantContextId())) { + return result; + } + if (result.failed()) { monitor.debug(() -> "Failed to resolve offer: %s".formatted(result.getFailureDetail())); - return ServiceResult.notFound("Not found"); } else { - if (participantContext.getParticipantContextId().equals(result.getContent().getContractDefinition().getParticipantContextId())) { - return result; - } monitor.debug(() -> "Offer %s does not belong to participantContext %s".formatted(offerId, participantContext.getParticipantContextId())); - return ServiceResult.notFound("Not found"); } + + return ServiceResult.notFound("Not found"); } - @NotNull - private ServiceResult validateAgreed(ContractAgreementMessage message, ParticipantAgent agent, ContractNegotiation negotiation) { - var agreement = message.getContractAgreement(); - var result = validationService.validateConfirmed(agent, agreement, negotiation.getLastContractOffer()); - if (result.failed()) { - var msg = "Contract agreement received. Validation failed: " + result.getFailureDetail(); - monitor.debug("[Consumer] " + msg); - return ServiceResult.badRequest(msg); + private ServiceResult requestedAction(ContractRequestMessage message, ContractNegotiation negotiation, ContractOffer contractOffer) { + if (negotiation.getType().equals(PROVIDER) && negotiation.canBeRequestedProvider()) { + negotiation.protocolMessageReceived(message.getId()); + negotiation.addContractOffer(contractOffer); + negotiation.transitionRequested(); + return update(negotiation) + .onSuccess(i -> observable.invokeForEach(l -> l.requested(negotiation))) + .map(i -> negotiation); } - return ServiceResult.success(agent); + return ServiceResult.conflict(format("Cannot process %s because %s", message.getClass().getSimpleName(), "negotiation cannot be requested")); } - @NotNull - private ServiceResult validateRequest(ParticipantAgent agent, ContractNegotiation negotiation) { - var result = validationService.validateRequest(agent, negotiation); - if (result.failed()) { - return ServiceResult.badRequest("Invalid client credentials: " + result.getFailureDetail()); - } else { - return ServiceResult.success(agent); + private ServiceResult offeredAction(ContractOfferMessage message, ContractNegotiation negotiation) { + if (negotiation.getType().equals(CONSUMER) && negotiation.canBeOfferedConsumer()) { + negotiation.protocolMessageReceived(message.getId()); + negotiation.addContractOffer(message.getContractOffer()); + negotiation.transitionOffered(); + return update(negotiation) + .onSuccess(i -> observable.invokeForEach(l -> l.offered(negotiation))) + .map(i -> negotiation); } + + return ServiceResult.conflict(format("Cannot process %s because %s", message.getClass().getSimpleName(), "negotiation cannot be offered")); } @NotNull @@ -304,15 +298,23 @@ private ServiceResult acceptedAction(ContractNegotiationEve @NotNull private ServiceResult agreedAction(ContractAgreementMessage message, ContractNegotiation negotiation, ParticipantAgent agent) { + var agreement = message.getContractAgreement(); + var result = validationService.validateConfirmed(agent, agreement, negotiation.getLastContractOffer()); + if (result.failed()) { + var msg = "Contract agreement received. Validation failed: " + result.getFailureDetail(); + monitor.debug("[Consumer] " + msg); + return ServiceResult.badRequest(msg); + } + if (negotiation.getType().equals(CONSUMER) && negotiation.canBeAgreedConsumer()) { - var agreement = message.getContractAgreement().toBuilder() + var agreementWithClaims = message.getContractAgreement().toBuilder() .participantContextId(negotiation.getParticipantContextId()) .claims(agent.getClaims()) .build(); negotiation.protocolMessageReceived(message.getId()); - negotiation.setContractAgreement(agreement); + negotiation.setContractAgreement(agreementWithClaims); negotiation.transitionAgreed(); update(negotiation); observable.invokeForEach(l -> l.agreed(negotiation)); @@ -364,23 +366,8 @@ private ServiceResult terminatedAction(ContractNegotiationT } } - private ServiceResult getAndLeaseNegotiation(ParticipantContext participantContext, String negotiationId) { - return store.findByIdAndLease(negotiationId) - .flatMap(ServiceResult::from) - .compose((cn) -> filterByParticipantContext(participantContext, cn)); - } - - private ServiceResult filterByParticipantContext(ParticipantContext participantContext, ContractNegotiation negotiation) { - if (negotiation.getParticipantContextId().equals(participantContext.getParticipantContextId())) { - return ServiceResult.success(negotiation); - } else { - return ServiceResult.notFound("No negotiation with id %s found".formatted(negotiation.getId())); - } - } - private ServiceResult verifyRequest(ParticipantContext participantContext, TokenRepresentation tokenRepresentation, Policy policy, RemoteMessage message) { - return protocolTokenValidator.verify(participantContext, tokenRepresentation, RequestContractNegotiationPolicyContext::new, policy, message) - .onFailure(failure -> monitor.debug(() -> "Verification Failed: %s".formatted(failure.getFailureDetail()))); + return protocolTokenValidator.verify(participantContext, tokenRepresentation, RequestContractNegotiationPolicyContext::new, policy, message); } private ServiceResult getNegotiation(ParticipantContext participantContext, String negotiationId) { @@ -390,14 +377,12 @@ private ServiceResult getNegotiation(ParticipantContext par .orElseGet(() -> ServiceResult.notFound("No negotiation with id %s found".formatted(negotiationId))); } - private void update(ContractNegotiation negotiation) { - store.save(negotiation); - monitor.debug(() -> "[%s] ContractNegotiation %s is now in state %s." - .formatted(negotiation.getType(), negotiation.getId(), negotiation.stateAsString())); - } + private ServiceResult update(ContractNegotiation negotiation) { + return store.save(negotiation) + .onSuccess(i -> monitor.debug(() -> "[%s] ContractNegotiation %s is now in state %s." + .formatted(negotiation.getType(), negotiation.getId(), negotiation.stateAsString()))) + .flatMap(ServiceResult::from); - private void breakLease(ContractNegotiation process) { - store.save(process); } } diff --git a/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/protocol/ProtocolTokenValidatorImpl.java b/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/protocol/ProtocolTokenValidatorImpl.java index 2f63ad0f4a6..8253f694755 100644 --- a/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/protocol/ProtocolTokenValidatorImpl.java +++ b/core/control-plane/control-plane-aggregate-services/src/main/java/org/eclipse/edc/connector/controlplane/services/protocol/ProtocolTokenValidatorImpl.java @@ -77,12 +77,10 @@ public ServiceResult verify(ParticipantContext participantCont var id = Optional.of(message.getProtocol()) .map(dataspaceProfileContextRegistry::getIdExtractionFunction) .map(extractor -> extractor.apply(claimToken)) - .orElseGet(() -> { - monitor.debug(() -> "Unauthorized: Cannot extract id on protocol [%s] from claims: %s.".formatted(message.getProtocol(), claimToken.getClaims())); - return null; - }); + .orElse(null); if (id == null) { + monitor.debug(() -> "Unauthorized: Cannot extract id on protocol [%s] from claims: %s.".formatted(message.getProtocol(), claimToken.getClaims())); return ServiceResult.unauthorized("Unauthorized"); } diff --git a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java index 8a4c09e8328..c00a5831ae0 100644 --- a/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java +++ b/core/control-plane/control-plane-aggregate-services/src/test/java/org/eclipse/edc/connector/controlplane/services/contractnegotiation/ContractNegotiationProtocolServiceImplTest.java @@ -34,12 +34,12 @@ import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractOffer; import org.eclipse.edc.connector.controlplane.contract.spi.validation.ContractValidationService; import org.eclipse.edc.connector.controlplane.contract.spi.validation.ValidatableConsumerOffer; -import org.eclipse.edc.connector.controlplane.contract.spi.validation.ValidatedConsumerOffer; import org.eclipse.edc.connector.controlplane.services.spi.contractnegotiation.ContractNegotiationProtocolService; import org.eclipse.edc.connector.controlplane.services.spi.protocol.ProtocolTokenValidator; import org.eclipse.edc.participant.spi.ParticipantAgent; import org.eclipse.edc.participantcontext.spi.types.ParticipantContext; import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.policy.model.PolicyType; import org.eclipse.edc.spi.iam.TokenRepresentation; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.result.ServiceFailure; @@ -102,7 +102,6 @@ class ContractNegotiationProtocolServiceImplTest { - private static final String CONSUMER_ID = "consumer"; private final ParticipantContext participantContext = ParticipantContext.Builder.newInstance() .participantContextId("participantContextId") .identity("participantId") @@ -117,12 +116,247 @@ class ContractNegotiationProtocolServiceImplTest { @BeforeEach void setUp() { + when(store.save(any())).thenReturn(StoreResult.success()); var observable = new ContractNegotiationObservableImpl(); observable.registerListener(listener); service = new ContractNegotiationProtocolServiceImpl(store, transactionContext, validationService, consumerOfferResolver, protocolTokenValidator, observable, mock(), mock()); } + @Nested + class NotifyRequested { + @Test + void shouldInitiateNegotiation_whenNegotiationDoesNotExist() { + var participantAgent = participantAgent(); + var tokenRepresentation = tokenRepresentation(); + var contractOffer = contractOffer(); + var message = ContractRequestMessage.Builder.newInstance() + .callbackAddress("callbackAddress") + .protocol("protocol") + .contractOffer(contractOffer) + .consumerPid("consumerPid") + .build(); + + var offerId = ContractOfferId.create("any", "any"); + var contractPolicy = createPolicy(); + var validatableOffer = ValidatableConsumerOffer.Builder.newInstance().offerId(offerId) + .accessPolicy(createPolicy()).contractPolicy(contractPolicy).contractDefinition(createContractDefinition()).build(); + when(consumerOfferResolver.resolveOffer(contractOffer.getId())).thenReturn(ServiceResult.success(validatableOffer)); + when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))).thenReturn(ServiceResult.success(participantAgent)); + when(store.findByIdAndLease(any())).thenReturn(StoreResult.notFound("not found")); + when(validationService.validateInitialOffer(participantAgent, validatableOffer)).thenReturn(Result.success()); + + var result = service.notifyRequested(participantContext, message, tokenRepresentation); + + assertThat(result).isSucceeded(); + var calls = ArgumentCaptor.forClass(ContractNegotiation.class); + verify(store, never()).findByIdAndLease(any()); + verify(store).save(calls.capture()); + assertThat(calls.getAllValues()).anySatisfy(n -> { + assertThat(n.getState()).isEqualTo(REQUESTED.code()); + assertThat(n.getCounterPartyAddress()).isEqualTo(message.getCallbackAddress()); + assertThat(n.getProtocol()).isEqualTo(message.getProtocol()); + assertThat(n.getCorrelationId()).isEqualTo(message.getConsumerPid()); + assertThat(n.getContractOffers()).hasSize(1); + assertThat(n.getLastContractOffer()).usingRecursiveComparison().isEqualTo(ContractOffer.Builder.newInstance() + .id(offerId.toString()) + .policy(contractPolicy.toBuilder().type(PolicyType.OFFER).target(offerId.assetIdPart()).build()) + .assetId(offerId.assetIdPart()) + .build()); + }); + verify(listener).requested(any()); + verify(validationService).validateInitialOffer(participantAgent, validatableOffer); + verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); + } + + @Test + void shouldTransitionToRequested_whenNegotiationFound() { + var participantAgent = participantAgent(); + var tokenRepresentation = tokenRepresentation(); + var contractOffer = contractOffer(); + var negotiation = contractNegotiationBuilder().state(OFFERED.code()).type(PROVIDER).contractOffer(contractOffer()).build(); + var message = ContractRequestMessage.Builder.newInstance() + .callbackAddress("callbackAddress") + .protocol("protocol") + .processId("consumerPid") + .contractOffer(contractOffer) + .consumerPid("consumerPid") + .providerPid("providerPid") + .build(); + + var offerId = ContractOfferId.create("any", "any"); + var contractPolicy = createPolicy(); + var validatableOffer = ValidatableConsumerOffer.Builder.newInstance().offerId(offerId) + .accessPolicy(createPolicy()).contractPolicy(contractPolicy).contractDefinition(createContractDefinition()).build(); + when(consumerOfferResolver.resolveOffer(contractOffer.getId())).thenReturn(ServiceResult.success(validatableOffer)); + when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))).thenReturn(ServiceResult.success(participantAgent)); + when(store.findById(any())).thenReturn(negotiation); + when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiation)); + when(validationService.validateInitialOffer(participantAgent, validatableOffer)).thenReturn(Result.success()); + when(validationService.validateRequest(any(), isA(ContractNegotiation.class))).thenReturn(Result.success()); + + var result = service.notifyRequested(participantContext, message, tokenRepresentation); + + assertThat(result).isSucceeded(); + verify(store).findByIdAndLease("consumerPid"); + var calls = ArgumentCaptor.forClass(ContractNegotiation.class); + verify(store).save(calls.capture()); + assertThat(calls.getAllValues()).anySatisfy(n -> { + assertThat(n.getState()).isEqualTo(REQUESTED.code()); + assertThat(n.getProtocol()).isEqualTo(message.getProtocol()); + assertThat(n.getContractOffers()).hasSize(2); + assertThat(n.getLastContractOffer()).usingRecursiveComparison().isEqualTo(ContractOffer.Builder.newInstance() + .id(offerId.toString()) + .policy(contractPolicy.toBuilder().type(PolicyType.OFFER).target(offerId.assetIdPart()).build()) + .assetId(offerId.assetIdPart()) + .build()); + }); + verify(listener).requested(any()); + verify(validationService).validateInitialOffer(participantAgent, validatableOffer); + verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); + } + + @Test + void shouldReturnNotFound_whenOfferNotFound() { + var tokenRepresentation = tokenRepresentation(); + var contractOffer = contractOffer(); + var message = ContractRequestMessage.Builder.newInstance() + .callbackAddress("callbackAddress") + .protocol("protocol") + .contractOffer(contractOffer) + .consumerPid("consumerPid") + .build(); + var validatableOffer = mock(ValidatableConsumerOffer.class); + + when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); + when(consumerOfferResolver.resolveOffer(contractOffer.getId())).thenReturn(ServiceResult.notFound("")); + + var result = service.notifyRequested(participantContext, message, tokenRepresentation); + + assertThat(result) + .isFailed() + .extracting(ServiceFailure::getReason) + .isEqualTo(NOT_FOUND); + } + + @Test + void shouldReturnBadRequest_whenInitialOfferIsNotValid() { + var message = ContractRequestMessage.Builder.newInstance() + .callbackAddress("callbackAddress") + .protocol("protocol") + .contractOffer(contractOffer()) + .consumerPid("consumerPid") + .build(); + + var validatableOffer = createValidatableConsumerOffer(); + when(consumerOfferResolver.resolveOffer(any())).thenReturn(ServiceResult.success(validatableOffer)); + when(protocolTokenValidator.verify(eq(participantContext), any(), any(), any(), eq(message))) + .thenReturn(ServiceResult.success(participantAgent())); + when(validationService.validateInitialOffer(any(), any())).thenReturn(Result.failure("inital offer not valid")); + + var result = service.notifyRequested(participantContext, message, tokenRepresentation()); + + assertThat(result).isFailed(); + verify(store, never()).findByIdAndLease(any()); + verify(store, never()).save(any()); + verifyNoInteractions(listener); + } + } + + @Nested + class NotifyOffered { + + @Test + void shouldInitiateNegotiation_whenNegotiationDoesNotExist() { + var tokenRepresentation = tokenRepresentation(); + var contractOffer = contractOffer(); + var message = ContractOfferMessage.Builder.newInstance() + .callbackAddress("callbackAddress") + .protocol("protocol") + .contractOffer(contractOffer) + .providerPid("providerPid") + .build(); + when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))) + .thenReturn(ServiceResult.success(participantAgent())); + + var result = service.notifyOffered(participantContext, message, tokenRepresentation); + + assertThat(result).isSucceeded(); + var calls = ArgumentCaptor.forClass(ContractNegotiation.class); + verify(store, never()).findByIdAndLease(any()); + verify(store).save(calls.capture()); + assertThat(calls.getAllValues()).anySatisfy(n -> { + assertThat(n.getState()).isEqualTo(OFFERED.code()); + assertThat(n.getType()).isEqualTo(CONSUMER); + assertThat(n.getCounterPartyId()).isEqualTo("counterPartyId"); + assertThat(n.getCounterPartyAddress()).isEqualTo(message.getCallbackAddress()); + assertThat(n.getProtocol()).isEqualTo(message.getProtocol()); + assertThat(n.getCorrelationId()).isEqualTo(message.getConsumerPid()); + assertThat(n.getContractOffers()).hasSize(1); + assertThat(n.getLastContractOffer()).isEqualTo(contractOffer); + }); + verify(listener).offered(any()); + verifyNoInteractions(validationService, consumerOfferResolver); + verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); + } + + @Test + void shouldTransitionToOffered_whenNegotiationAlreadyExist() { + var processId = "processId"; + var participantAgent = participantAgent(); + var tokenRepresentation = tokenRepresentation(); + var contractOffer = contractOffer(); + var message = ContractOfferMessage.Builder.newInstance() + .callbackAddress("callbackAddress") + .protocol("protocol") + .contractOffer(contractOffer) + .processId("providerPid") + .consumerPid("consumerPid") + .providerPid("providerPid") + .build(); + var negotiation = contractNegotiationBuilder().type(CONSUMER).state(REQUESTED.code()).contractOffer(contractOffer()).build(); + + when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))) + .thenReturn(ServiceResult.success(participantAgent)); + when(store.findById(processId)).thenReturn(negotiation); + when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiation)); + when(validationService.validateRequest(participantAgent, negotiation)).thenReturn(Result.success()); + + var result = service.notifyOffered(participantContext, message, tokenRepresentation); + + assertThat(result).isSucceeded(); + var updatedNegotiation = result.getContent(); + assertThat(updatedNegotiation.getContractOffers()).hasSize(2); + assertThat(updatedNegotiation.getLastContractOffer()).isEqualTo(contractOffer); + verify(store).findByIdAndLease("providerPid"); + verify(listener).offered(any()); + verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); + } + + @Test + void shouldReturnNotFound_whenOfferNotFound() { + var tokenRepresentation = tokenRepresentation(); + var contractOffer = contractOffer(); + var message = ContractOfferMessage.Builder.newInstance() + .callbackAddress("callbackAddress") + .protocol("protocol") + .contractOffer(contractOffer) + .consumerPid("consumerPid") + .providerPid("providerPid") + .build(); + when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))) + .thenReturn(ServiceResult.success(participantAgent())); + when(store.findByIdAndLease(any())).thenReturn(StoreResult.notFound("not found")); + + var result = service.notifyOffered(participantContext, message, tokenRepresentation); + + assertThat(result) + .isFailed() + .extracting(ServiceFailure::getReason) + .isEqualTo(NOT_FOUND); + } + } + @Test void notifyAccepted_shouldTransitionToAccepted() { var contractNegotiation = createContractNegotiationOffered(); @@ -159,7 +393,8 @@ class NotifyAgreed { @Test void shouldTransitionToAgreed() { - var negotiationConsumerRequested = createContractNegotiationRequested(); + var negotiationConsumerRequested = contractNegotiationBuilder().type(CONSUMER).state(REQUESTED.code()) + .contractOffer(contractOffer()).build(); var participantAgent = new ParticipantAgent("counterPartyId", Map.of("claim", "value"), emptyMap()); var tokenRepresentation = tokenRepresentation(); @@ -177,6 +412,7 @@ void shouldTransitionToAgreed() { when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))).thenReturn(ServiceResult.success(participantAgent)); when(store.findById(any())).thenReturn(negotiationConsumerRequested); when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiationConsumerRequested)); + when(validationService.validateRequest(eq(participantAgent), any(ContractNegotiation.class))).thenReturn(Result.success()); when(validationService.validateConfirmed(eq(participantAgent), eq(contractAgreement), any(ContractOffer.class))).thenReturn(Result.success()); var result = service.notifyAgreed(participantContext, message, tokenRepresentation); @@ -386,25 +622,23 @@ void notify_shouldReturnNotFound_whenNotFound(MethodCa @ParameterizedTest @ArgumentsSource(NotifyArguments.class) - void notify_shouldReturnBadRequest_whenValidationFails(MethodCall methodCall, M message) { + void notify_shouldReturnBadRequest_whenRequestValidationFails(MethodCall methodCall, M message) { var tokenRepresentation = tokenRepresentation(); - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); + var validatableOffer = createValidatableConsumerOffer(); when(consumerOfferResolver.resolveOffer(any())).thenReturn(ServiceResult.success(validatableOffer)); when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))) .thenReturn(ServiceResult.success(participantAgent())); when(store.findById(any())).thenReturn(createContractNegotiationOffered()); when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(createContractNegotiationOffered())); - when(validationService.validateRequest(any(ParticipantAgent.class), any(ContractNegotiation.class))).thenReturn(Result.failure("validation error")); - when(validationService.validateInitialOffer(any(ParticipantAgent.class), isA(ValidatableConsumerOffer.class))).thenReturn(Result.failure("error")); - when(validationService.validateConfirmed(any(ParticipantAgent.class), any(), any(ContractOffer.class))).thenReturn(Result.failure("failure")); + when(validationService.validateRequest(any(ParticipantAgent.class), any(ContractNegotiation.class))) + .thenReturn(Result.failure("validation error")); + when(validationService.validateInitialOffer(any(ParticipantAgent.class), isA(ValidatableConsumerOffer.class))) + .thenReturn(Result.success()); var result = methodCall.call(service, participantContext, message, tokenRepresentation); assertThat(result).isFailed().extracting(ServiceFailure::getReason).isEqualTo(BAD_REQUEST); - verify(store, never()).save(any()); + verify(store).breakLease(any()); verifyNoInteractions(listener); } @@ -456,21 +690,12 @@ private ParticipantAgent participantAgent() { return new ParticipantAgent("counterPartyId", emptyMap(), emptyMap()); } - private ContractNegotiation createContractNegotiationRequested() { - var lastOffer = contractOffer(); - - return contractNegotiationBuilder() - .state(REQUESTED.code()) - .contractOffer(lastOffer) - .build(); - } - private ContractNegotiation createContractNegotiationOffered() { var lastOffer = contractOffer(); return contractNegotiationBuilder() .state(OFFERED.code()) - .type(PROVIDER) + .type(CONSUMER) .contractOffer(lastOffer).build(); } @@ -491,10 +716,6 @@ private TokenRepresentation tokenRepresentation() { .build(); } - @FunctionalInterface - private interface MethodCall { - ServiceResult call(ContractNegotiationProtocolService service, ParticipantContext participantContext, M message, TokenRepresentation token); - } interface TestFunctions { static ContractOffer contractOffer() { @@ -590,211 +811,6 @@ private static class NotifyArguments implements ArgumentsProvider { } - @Nested - class NotifyRequested { - @Test - void shouldInitiateNegotiation_whenNegotiationDoesNotExist() { - var participantAgent = participantAgent(); - var tokenRepresentation = tokenRepresentation(); - var contractOffer = contractOffer(); - var validatedOffer = new ValidatedConsumerOffer(CONSUMER_ID, contractOffer); - var message = ContractRequestMessage.Builder.newInstance() - .callbackAddress("callbackAddress") - .protocol("protocol") - .contractOffer(contractOffer) - .consumerPid("consumerPid") - .build(); - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); - when(consumerOfferResolver.resolveOffer(contractOffer.getId())).thenReturn(ServiceResult.success(validatableOffer)); - when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))).thenReturn(ServiceResult.success(participantAgent)); - when(store.findByIdAndLease(any())).thenReturn(StoreResult.notFound("not found")); - when(validationService.validateInitialOffer(participantAgent, validatableOffer)).thenReturn(Result.success(validatedOffer)); - - var result = service.notifyRequested(participantContext, message, tokenRepresentation); - - assertThat(result).isSucceeded(); - var calls = ArgumentCaptor.forClass(ContractNegotiation.class); - verify(store, never()).findByIdAndLease(any()); - verify(store).save(calls.capture()); - assertThat(calls.getAllValues()).anySatisfy(n -> { - assertThat(n.getState()).isEqualTo(REQUESTED.code()); - assertThat(n.getCounterPartyAddress()).isEqualTo(message.getCallbackAddress()); - assertThat(n.getProtocol()).isEqualTo(message.getProtocol()); - assertThat(n.getCorrelationId()).isEqualTo(message.getConsumerPid()); - assertThat(n.getContractOffers()).hasSize(1); - assertThat(n.getLastContractOffer()).isEqualTo(contractOffer); - }); - verify(listener).requested(any()); - verify(validationService).validateInitialOffer(participantAgent, validatableOffer); - verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); - } - - @Test - void shouldTransitionToRequested_whenNegotiationFound() { - var participantAgent = participantAgent(); - var tokenRepresentation = tokenRepresentation(); - var contractOffer = contractOffer(); - var validatedOffer = new ValidatedConsumerOffer(CONSUMER_ID, contractOffer); - var negotiation = createContractNegotiationOffered(); - var message = ContractRequestMessage.Builder.newInstance() - .callbackAddress("callbackAddress") - .protocol("protocol") - .processId("processId") - .contractOffer(contractOffer) - .consumerPid("consumerPid") - .providerPid("providerPid") - .build(); - - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); - when(consumerOfferResolver.resolveOffer(contractOffer.getId())).thenReturn(ServiceResult.success(validatableOffer)); - when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))).thenReturn(ServiceResult.success(participantAgent)); - when(store.findById(any())).thenReturn(negotiation); - when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiation)); - when(validationService.validateInitialOffer(participantAgent, validatableOffer)).thenReturn(Result.success(validatedOffer)); - - - var result = service.notifyRequested(participantContext, message, tokenRepresentation); - - assertThat(result).isSucceeded(); - verify(store).findByIdAndLease("providerPid"); - var calls = ArgumentCaptor.forClass(ContractNegotiation.class); - verify(store).save(calls.capture()); - assertThat(calls.getAllValues()).anySatisfy(n -> { - assertThat(n.getState()).isEqualTo(REQUESTED.code()); - assertThat(n.getProtocol()).isEqualTo(message.getProtocol()); - assertThat(n.getContractOffers()).hasSize(2); - assertThat(n.getLastContractOffer()).isEqualTo(contractOffer); - }); - verify(listener).requested(any()); - verify(store).findByIdAndLease("providerPid"); - verify(validationService).validateInitialOffer(participantAgent, validatableOffer); - verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); - } - - @Test - void shouldReturnNotFound_whenOfferNotFound() { - var tokenRepresentation = tokenRepresentation(); - var contractOffer = contractOffer(); - var message = ContractRequestMessage.Builder.newInstance() - .callbackAddress("callbackAddress") - .protocol("protocol") - .contractOffer(contractOffer) - .consumerPid("consumerPid") - .build(); - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(consumerOfferResolver.resolveOffer(contractOffer.getId())).thenReturn(ServiceResult.notFound("")); - - var result = service.notifyRequested(participantContext, message, tokenRepresentation); - - assertThat(result) - .isFailed() - .extracting(ServiceFailure::getReason) - .isEqualTo(NOT_FOUND); - } - } - - @Nested - class NotifyOffered { - - @Test - void shouldInitiateNegotiation_whenNegotiationDoesNotExist() { - var tokenRepresentation = tokenRepresentation(); - var contractOffer = contractOffer(); - var message = ContractOfferMessage.Builder.newInstance() - .callbackAddress("callbackAddress") - .protocol("protocol") - .contractOffer(contractOffer) - .providerPid("providerPid") - .build(); - when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))) - .thenReturn(ServiceResult.success(participantAgent())); - - var result = service.notifyOffered(participantContext, message, tokenRepresentation); - - assertThat(result).isSucceeded(); - var calls = ArgumentCaptor.forClass(ContractNegotiation.class); - verify(store, never()).findByIdAndLease(any()); - verify(store).save(calls.capture()); - assertThat(calls.getAllValues()).anySatisfy(n -> { - assertThat(n.getState()).isEqualTo(OFFERED.code()); - assertThat(n.getType()).isEqualTo(CONSUMER); - assertThat(n.getCounterPartyId()).isEqualTo("counterPartyId"); - assertThat(n.getCounterPartyAddress()).isEqualTo(message.getCallbackAddress()); - assertThat(n.getProtocol()).isEqualTo(message.getProtocol()); - assertThat(n.getCorrelationId()).isEqualTo(message.getConsumerPid()); - assertThat(n.getContractOffers()).hasSize(1); - assertThat(n.getLastContractOffer()).isEqualTo(contractOffer); - }); - verify(listener).offered(any()); - verifyNoInteractions(validationService, consumerOfferResolver); - verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); - } - - @Test - void shouldTransitionToOffered_whenNegotiationAlreadyExist() { - var processId = "processId"; - var participantAgent = participantAgent(); - var tokenRepresentation = tokenRepresentation(); - var contractOffer = contractOffer(); - var message = ContractOfferMessage.Builder.newInstance() - .callbackAddress("callbackAddress") - .protocol("protocol") - .contractOffer(contractOffer) - .processId("providerPid") - .consumerPid("consumerPid") - .providerPid("providerPid") - .build(); - var negotiation = createContractNegotiationRequested(); - - when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))) - .thenReturn(ServiceResult.success(participantAgent)); - when(store.findById(processId)).thenReturn(negotiation); - when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiation)); - when(validationService.validateRequest(participantAgent, negotiation)).thenReturn(Result.success()); - - var result = service.notifyOffered(participantContext, message, tokenRepresentation); - - assertThat(result).isSucceeded(); - var updatedNegotiation = result.getContent(); - assertThat(updatedNegotiation.getContractOffers()).hasSize(2); - assertThat(updatedNegotiation.getLastContractOffer()).isEqualTo(contractOffer); - verify(store).findByIdAndLease("consumerPid"); - verify(listener).offered(any()); - verify(transactionContext, atLeastOnce()).execute(any(TransactionContext.ResultTransactionBlock.class)); - } - - @Test - void shouldReturnNotFound_whenOfferNotFound() { - var tokenRepresentation = tokenRepresentation(); - var contractOffer = contractOffer(); - var message = ContractOfferMessage.Builder.newInstance() - .callbackAddress("callbackAddress") - .protocol("protocol") - .contractOffer(contractOffer) - .consumerPid("consumerPid") - .providerPid("providerPid") - .build(); - when(protocolTokenValidator.verify(eq(participantContext), eq(tokenRepresentation), any(), any(), eq(message))) - .thenReturn(ServiceResult.success(participantAgent())); - when(store.findByIdAndLease(any())).thenReturn(StoreResult.notFound("not found")); - - var result = service.notifyOffered(participantContext, message, tokenRepresentation); - - assertThat(result) - .isFailed() - .extracting(ServiceFailure::getReason) - .isEqualTo(NOT_FOUND); - } - } - @Nested class IdempotencyProcessStateReplication { @@ -806,10 +822,7 @@ void notify_shouldStoreReceivedMessageId(Method var offer = contractOffer(); var negotiation = contractNegotiationBuilder().state(currentState.code()).type(type).contractOffer(offer) .contractAgreement(createContractAgreementBuilder().build()).build(); - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); + var validatableOffer = createValidatableConsumerOffer(); when(consumerOfferResolver.resolveOffer(any())).thenReturn(ServiceResult.success(validatableOffer)); when(protocolTokenValidator.verify(any(), any(), any(), any(), eq(message))) .thenReturn(ServiceResult.success(participantAgent())); @@ -817,7 +830,7 @@ void notify_shouldStoreReceivedMessageId(Method when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiation)); when(validationService.validateRequest(any(ParticipantAgent.class), any(ContractNegotiation.class))).thenReturn(Result.success()); when(validationService.validateInitialOffer(any(ParticipantAgent.class), isA(ValidatableConsumerOffer.class))) - .thenAnswer(i -> Result.success(new ValidatedConsumerOffer("any", offer))); + .thenAnswer(i -> Result.success()); when(validationService.validateConfirmed(any(ParticipantAgent.class), any(), any())).thenAnswer(i -> Result.success(negotiation)); var result = methodCall.call(service, participantContext, message, tokenRepresentation()); @@ -837,10 +850,7 @@ void notify_shouldIgnoreMessage_whenAlreadyRece var offer = contractOffer(); var negotiation = contractNegotiationBuilder().state(currentState.code()).type(type).contractOffer(offer).build(); negotiation.protocolMessageReceived(message.getId()); - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); + var validatableOffer = createValidatableConsumerOffer(); when(consumerOfferResolver.resolveOffer(any())).thenReturn(ServiceResult.success(validatableOffer)); when(protocolTokenValidator.verify(any(), any(), any(), any(), eq(message))) .thenReturn(ServiceResult.success(participantAgent())); @@ -849,13 +859,13 @@ void notify_shouldIgnoreMessage_whenAlreadyRece when(store.breakLease(any())).thenReturn(StoreResult.success()); when(validationService.validateRequest(any(ParticipantAgent.class), any(ContractNegotiation.class))).thenReturn(Result.success()); when(validationService.validateInitialOffer(any(ParticipantAgent.class), any(ValidatableConsumerOffer.class))) - .thenAnswer(i -> Result.success(new ValidatedConsumerOffer("any", offer))); + .thenAnswer(i -> Result.success()); when(validationService.validateConfirmed(any(ParticipantAgent.class), any(), any())).thenAnswer(i -> Result.success(negotiation)); var result = methodCall.call(service, participantContext, message, tokenRepresentation()); assertThat(result).isSucceeded(); - verify(store, never()).save(any()); + verify(store).breakLease(negotiation); verifyNoInteractions(listener); } @@ -865,11 +875,7 @@ void notify_shouldReturnConflict_whenFinalizedS ContractNegotiation.Type type) { var offer = contractOffer(); var negotiation = contractNegotiationBuilder().state(FINALIZED.code()).type(type).contractOffer(offer).build(); - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); + var validatableOffer = createValidatableConsumerOffer(); when(consumerOfferResolver.resolveOffer(any())).thenReturn(ServiceResult.success(validatableOffer)); when(protocolTokenValidator.verify(any(), any(), any(), any(), eq(message))) .thenReturn(ServiceResult.success(participantAgent())); @@ -877,7 +883,7 @@ void notify_shouldReturnConflict_whenFinalizedS when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiation)); when(validationService.validateRequest(any(ParticipantAgent.class), any(ContractNegotiation.class))).thenReturn(Result.success()); when(validationService.validateInitialOffer(any(ParticipantAgent.class), any(ValidatableConsumerOffer.class))) - .thenAnswer(i -> Result.success(new ValidatedConsumerOffer("any", offer))); + .thenAnswer(i -> Result.success()); when(validationService.validateConfirmed(any(ParticipantAgent.class), any(), any())).thenAnswer(i -> Result.success(negotiation)); var result = methodCall.call(service, participantContext, message, tokenRepresentation()); @@ -894,10 +900,7 @@ void notify_shouldReturnConflict_whenTerminated ContractNegotiation.Type type) { var offer = contractOffer(); var negotiation = contractNegotiationBuilder().state(TERMINATED.code()).type(type).contractOffer(offer).build(); - var validatableOffer = mock(ValidatableConsumerOffer.class); - - when(validatableOffer.getContractPolicy()).thenReturn(createPolicy()); - when(validatableOffer.getContractDefinition()).thenReturn(createContractDefinition()); + var validatableOffer = createValidatableConsumerOffer(); when(consumerOfferResolver.resolveOffer(any())).thenReturn(ServiceResult.success(validatableOffer)); when(protocolTokenValidator.verify(any(), any(), any(), any(), eq(message))) .thenReturn(ServiceResult.success(participantAgent())); @@ -905,7 +908,7 @@ void notify_shouldReturnConflict_whenTerminated when(store.findByIdAndLease(any())).thenReturn(StoreResult.success(negotiation)); when(validationService.validateRequest(any(ParticipantAgent.class), any(ContractNegotiation.class))).thenReturn(Result.success()); when(validationService.validateInitialOffer(any(ParticipantAgent.class), any(ValidatableConsumerOffer.class))) - .thenAnswer(i -> Result.success(new ValidatedConsumerOffer("any", offer))); + .thenAnswer(i -> Result.success()); when(validationService.validateConfirmed(any(ParticipantAgent.class), any(), any())).thenAnswer(i -> Result.success(negotiation)); var result = methodCall.call(service, participantContext, message, tokenRepresentation()); @@ -917,6 +920,14 @@ void notify_shouldReturnConflict_whenTerminated } } + private ValidatableConsumerOffer createValidatableConsumerOffer() { + return ValidatableConsumerOffer.Builder.newInstance() + .offerId(ContractOfferId.create(UUID.randomUUID().toString(), UUID.randomUUID().toString())) + .accessPolicy(createPolicy()).contractPolicy(createPolicy()) + .contractDefinition(createContractDefinition()) + .build(); + } + private ContractAgreement.Builder createContractAgreementBuilder() { return ContractAgreement.Builder.newInstance() .providerId("providerId") @@ -926,4 +937,8 @@ private ContractAgreement.Builder createContractAgreementBuilder() { .participantContextId("participantContextId"); } + @FunctionalInterface + private interface MethodCall { + ServiceResult call(ContractNegotiationProtocolService service, ParticipantContext participantContext, M message, TokenRepresentation token); + } } diff --git a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImpl.java b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImpl.java index 8eba1be3fa8..345f6df88b6 100644 --- a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImpl.java +++ b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImpl.java @@ -41,31 +41,33 @@ public ConsumerOfferResolverImpl(ContractDefinitionStore contractDefinitionStore if (parsedResult.failed()) { return ServiceResult.badRequest(parsedResult.getFailureMessages()); - } else { - var definitionId = parsedResult.getContent().definitionPart(); - var contractDefinition = contractDefinitionStore.findById(definitionId); - if (contractDefinition == null) { - return ServiceResult.notFound(format("Contract definition with id %s not found", definitionId)); - } - - var accessPolicy = policyDefinitionStore.findById(contractDefinition.getAccessPolicyId()); - if (accessPolicy == null) { - return ServiceResult.notFound(format("Policy with id %s not found", contractDefinition.getAccessPolicyId())); - } - - var contractPolicy = policyDefinitionStore.findById(contractDefinition.getContractPolicyId()); - if (contractPolicy == null) { - return ServiceResult.notFound(format("Policy with id %s not found", contractDefinition.getContractPolicyId())); - } - - return ServiceResult.success(ValidatableConsumerOffer.Builder.newInstance() - .contractDefinition(contractDefinition) - .accessPolicy(accessPolicy.getPolicy()) - .contractPolicy(contractPolicy.getPolicy()) - .offerId(parsedResult.getContent()) - .build()); + } + + var parsedOfferId = parsedResult.getContent(); + var definitionId = parsedOfferId.definitionPart(); + var contractDefinition = contractDefinitionStore.findById(definitionId); + if (contractDefinition == null) { + return ServiceResult.notFound(format("Contract definition with id %s not found", definitionId)); + } + + var accessPolicy = policyDefinitionStore.findById(contractDefinition.getAccessPolicyId()); + if (accessPolicy == null) { + return ServiceResult.notFound(format("Policy with id %s not found", contractDefinition.getAccessPolicyId())); + } + var contractPolicy = policyDefinitionStore.findById(contractDefinition.getContractPolicyId()); + if (contractPolicy == null) { + return ServiceResult.notFound(format("Policy with id %s not found", contractDefinition.getContractPolicyId())); } + + return ServiceResult.success(ValidatableConsumerOffer.Builder.newInstance() + .contractDefinition(contractDefinition) + .accessPolicy(accessPolicy.getPolicy()) + .contractPolicy(contractPolicy.getPolicy()) + .offerId(parsedOfferId) + .build()); + + } } diff --git a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImpl.java b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImpl.java index ed6b1a86b75..507fcc9f29c 100644 --- a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImpl.java +++ b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImpl.java @@ -20,7 +20,6 @@ import org.eclipse.edc.connector.controlplane.asset.spi.index.AssetIndex; import org.eclipse.edc.connector.controlplane.catalog.spi.policy.CatalogPolicyContext; import org.eclipse.edc.connector.controlplane.contract.policy.PolicyEquality; -import org.eclipse.edc.connector.controlplane.contract.spi.ContractOfferId; import org.eclipse.edc.connector.controlplane.contract.spi.policy.ContractNegotiationPolicyContext; import org.eclipse.edc.connector.controlplane.contract.spi.policy.TransferProcessPolicyContext; import org.eclipse.edc.connector.controlplane.contract.spi.types.agreement.ContractAgreement; @@ -28,11 +27,9 @@ import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractOffer; import org.eclipse.edc.connector.controlplane.contract.spi.validation.ContractValidationService; import org.eclipse.edc.connector.controlplane.contract.spi.validation.ValidatableConsumerOffer; -import org.eclipse.edc.connector.controlplane.contract.spi.validation.ValidatedConsumerOffer; import org.eclipse.edc.participant.spi.ParticipantAgent; import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.model.Policy; -import org.eclipse.edc.policy.model.PolicyType; import org.eclipse.edc.spi.query.Criterion; import org.eclipse.edc.spi.result.Result; import org.jetbrains.annotations.NotNull; @@ -63,10 +60,36 @@ public ContractValidationServiceImpl(AssetIndex assetIndex, } @Override - public @NotNull Result validateInitialOffer(ParticipantAgent agent, ValidatableConsumerOffer consumerOffer) { - return validateInitialOffer(consumerOffer, agent) - .compose(policy -> createContractOffer(policy, consumerOffer.getOfferId())) - .map(contractOffer -> new ValidatedConsumerOffer(agent.getIdentity(), contractOffer)); + public @NotNull Result validateInitialOffer(ParticipantAgent agent, ValidatableConsumerOffer consumerOffer) { + var accessPolicyResult = policyEngine.evaluate(consumerOffer.getAccessPolicy(), new CatalogPolicyContext(agent)); + if (accessPolicyResult.failed()) { + return accessPolicyResult.mapFailure(); + } + + var target = consumerOffer.getOfferId().assetIdPart(); + if (assetIndex.findById(target) == null) { + return failure("Invalid target: " + target); + } + + // verify that the asset in the offer is actually in the contract definition + var testCriteria = new ArrayList<>(consumerOffer.getContractDefinition().getAssetsSelector()); + testCriteria.add(new Criterion(Asset.PROPERTY_ID, "=", target)); + if (assetIndex.countAssets(testCriteria) <= 0) { + return failure("Asset ID from the ContractOffer is not included in the ContractDefinition"); + } + + var contractOfferId = consumerOffer.getOfferId(); + if (!contractOfferId.assetIdPart().equals(target)) { + return failure("Policy target %s does not match the asset ID in the contract offer %s".formatted(target, contractOfferId.assetIdPart())); + } + + var contractPolicy = consumerOffer.getTargetedContractPolicy(); + var contractPolicyResult = policyEngine.evaluate(contractPolicy, new ContractNegotiationPolicyContext(agent)); + if (contractPolicyResult.failed()) { + return contractPolicyResult.mapFailure(); + } + + return Result.success(); } @Override @@ -130,34 +153,27 @@ private Result validateInitialOffer(ValidatableConsumerOffer consumerOff } // verify the target asset exists - var targetAsset = assetIndex.findById(consumerOffer.getOfferId().assetIdPart()); + var target = consumerOffer.getOfferId().assetIdPart(); + var targetAsset = assetIndex.findById(target); if (targetAsset == null) { - return failure("Invalid target: " + consumerOffer.getOfferId().assetIdPart()); + return failure("Invalid target: " + target); } // verify that the asset in the offer is actually in the contract definition var testCriteria = new ArrayList<>(consumerOffer.getContractDefinition().getAssetsSelector()); - testCriteria.add(new Criterion(Asset.PROPERTY_ID, "=", consumerOffer.getOfferId().assetIdPart())); + testCriteria.add(new Criterion(Asset.PROPERTY_ID, "=", target)); if (assetIndex.countAssets(testCriteria) <= 0) { return failure("Asset ID from the ContractOffer is not included in the ContractDefinition"); } - var contractPolicy = consumerOffer.getContractPolicy().withTarget(consumerOffer.getOfferId().assetIdPart()); + var contractOfferId = consumerOffer.getOfferId(); + if (!contractOfferId.assetIdPart().equals(target)) { + return failure("Policy target %s does not match the asset ID in the contract offer %s".formatted(target, contractOfferId.assetIdPart())); + } + + var contractPolicy = consumerOffer.getContractPolicy().withTarget(target); return policyEngine.evaluate(contractPolicy, new ContractNegotiationPolicyContext(agent)) .map(v -> contractPolicy); } - @NotNull - private Result createContractOffer(Policy policy, ContractOfferId contractOfferId) { - if (!contractOfferId.assetIdPart().equals(policy.getTarget())) { - return Result.failure("Policy target %s does not match the asset ID in the contract offer %s".formatted(policy.getTarget(), contractOfferId.assetIdPart())); - } - return Result.success(ContractOffer.Builder.newInstance() - .id(contractOfferId.toString()) - // we copy the policy and enforce it to be of type OFFER - .policy(policy.toBuilder().type(PolicyType.OFFER).build()) - .assetId(contractOfferId.assetIdPart()) - .build()); - } - } diff --git a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImplTest.java b/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImplTest.java index 9c1d220a152..d73eb1a216c 100644 --- a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImplTest.java +++ b/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer/ConsumerOfferResolverImplTest.java @@ -37,8 +37,8 @@ class ConsumerOfferResolverImplTest { - private final PolicyDefinitionStore policyStore = mock(PolicyDefinitionStore.class); - private final ContractDefinitionStore definitionStore = mock(ContractDefinitionStore.class); + private final PolicyDefinitionStore policyStore = mock(); + private final ContractDefinitionStore definitionStore = mock(); private ConsumerOfferResolverImpl validatableConsumerOfferResolver; @BeforeEach diff --git a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImplTest.java b/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImplTest.java index dff321e0bd0..541dc5abea2 100644 --- a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImplTest.java +++ b/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/validation/ContractValidationServiceImplTest.java @@ -34,7 +34,6 @@ import org.eclipse.edc.policy.engine.spi.PolicyContext; import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.model.Policy; -import org.eclipse.edc.policy.model.PolicyType; import org.eclipse.edc.spi.result.Result; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; @@ -105,12 +104,6 @@ void verifyContractOfferValidation() { var result = validationService.validateInitialOffer(participantAgent, validatableOffer); assertThat(result.succeeded()).isTrue(); - var validatedOffer = result.getContent().getOffer(); - assertThat(validatedOffer.getPolicy()).isNotSameAs(originalPolicy); // verify the returned policy is the sanitized one - assertThat(validatedOffer.getPolicy().getType()).isEqualTo(PolicyType.OFFER); // verify the returned policy is of type OFFER - assertThat(validatedOffer.getAssetId()).isEqualTo(asset.getId()); - - assertThat(result.getContent().getConsumerIdentity()).isEqualTo(CONSUMER_ID); // verify the returned policy has the consumer id set, essential for later validation checks verify(assetIndex).findById("1"); verify(policyEngine).evaluate( diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ContractValidationService.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ContractValidationService.java index 8cd7ed9eec4..4fa43f90151 100644 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ContractValidationService.java +++ b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ContractValidationService.java @@ -34,10 +34,10 @@ public interface ContractValidationService { * * @param agent The {@link ParticipantAgent} of the consumer * @param consumerOffer The initial {@link ValidatableConsumerOffer} id to validate - * @return The referenced {@link ValidatedConsumerOffer}. + * @return the validation result. */ @NotNull - Result validateInitialOffer(ParticipantAgent agent, ValidatableConsumerOffer consumerOffer); + Result validateInitialOffer(ParticipantAgent agent, ValidatableConsumerOffer consumerOffer); /** * Validates the contract agreement that the consumer referenced in its transfer request. diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatableConsumerOffer.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatableConsumerOffer.java index dc5349f8e82..7de21dc2678 100644 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatableConsumerOffer.java +++ b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatableConsumerOffer.java @@ -50,6 +50,10 @@ public ContractDefinition getContractDefinition() { return contractDefinition; } + public Policy getTargetedContractPolicy() { + return contractPolicy.withTarget(offerId.assetIdPart()); + } + public static final class Builder { private final ValidatableConsumerOffer consumerOffer; diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatedConsumerOffer.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatedConsumerOffer.java deleted file mode 100644 index a8a9227e78e..00000000000 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/validation/ValidatedConsumerOffer.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - * - * 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: - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation - * - */ - -package org.eclipse.edc.connector.controlplane.contract.spi.validation; - -import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractOffer; - -import static java.util.Objects.requireNonNull; - -/** - * A validated contract offer received from a client. Specifically, the consumer identity and offer have been determined to be - * trustworthy and correct. - */ -public class ValidatedConsumerOffer { - private final String consumerIdentity; - private final ContractOffer offer; - - public ValidatedConsumerOffer(String consumerIdentity, ContractOffer offer) { - requireNonNull(consumerIdentity, "clientId"); - requireNonNull(offer, "offer"); - this.consumerIdentity = consumerIdentity; - this.offer = offer; - } - - public String getConsumerIdentity() { - return consumerIdentity; - } - - public ContractOffer getOffer() { - return offer; - } - -}