From f68827462e726f8853929f2398b92bc637200723 Mon Sep 17 00:00:00 2001 From: andrea bertagnolli Date: Wed, 2 Oct 2024 08:54:57 +0200 Subject: [PATCH] refactor: avoid potential unnecessary store accesses on dataset resolution (#4513) * refactor: avoid potential unnecessary store accesses on dataset resolution * pr remarks --- .../catalog/CatalogCoreExtension.java | 16 +- .../CatalogDefaultServicesExtension.java | 1 + .../ContractDefinitionResolverImpl.java | 74 +++ .../catalog/DatasetResolverImpl.java | 30 +- .../ContractDefinitionResolverImplTest.java | 94 ++-- .../DatasetResolverImplIntegrationTest.java | 17 +- .../catalog/DatasetResolverImplTest.java | 515 +++++++++--------- .../contract/ContractCoreExtension.java | 4 + ...ctNegotiationDefaultServicesExtension.java | 12 - .../offer/ContractDefinitionResolverImpl.java | 90 --- .../ContractValidationServiceImpl.java | 4 +- .../ContractValidationServiceImplTest.java | 10 +- .../catalog-spi/build.gradle.kts | 2 + .../spi/ContractDefinitionResolver.java | 36 ++ .../spi/ResolvedContractDefinitions.java | 35 ++ .../spi/offer/ContractDefinitionResolver.java | 49 -- 16 files changed, 512 insertions(+), 477 deletions(-) create mode 100644 core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/ContractDefinitionResolverImpl.java rename core/control-plane/{control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer => control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog}/ContractDefinitionResolverImplTest.java (58%) delete mode 100644 core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ContractDefinitionResolverImpl.java create mode 100644 spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ContractDefinitionResolver.java create mode 100644 spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ResolvedContractDefinitions.java delete mode 100644 spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/offer/ContractDefinitionResolver.java diff --git a/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogCoreExtension.java b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogCoreExtension.java index 92d35568887..249a66301ca 100644 --- a/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogCoreExtension.java +++ b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogCoreExtension.java @@ -17,8 +17,10 @@ import org.eclipse.edc.connector.controlplane.asset.spi.index.AssetIndex; import org.eclipse.edc.connector.controlplane.catalog.spi.DatasetResolver; import org.eclipse.edc.connector.controlplane.catalog.spi.DistributionResolver; -import org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver; +import org.eclipse.edc.connector.controlplane.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.PolicyScope; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; @@ -30,8 +32,8 @@ public class CatalogCoreExtension implements ServiceExtension { public static final String NAME = "Catalog Core"; - @Inject - private ContractDefinitionResolver contractDefinitionResolver; + @PolicyScope + public static final String CATALOG_SCOPE = "catalog"; @Inject private AssetIndex assetIndex; @@ -45,6 +47,12 @@ public class CatalogCoreExtension implements ServiceExtension { @Inject private CriterionOperatorRegistry criterionOperatorRegistry; + @Inject + private ContractDefinitionStore contractDefinitionStore; + + @Inject + private PolicyEngine policyEngine; + @Override public String name() { return NAME; @@ -52,7 +60,9 @@ public String name() { @Provider public DatasetResolver datasetResolver() { + var contractDefinitionResolver = new ContractDefinitionResolverImpl(contractDefinitionStore, policyEngine, policyDefinitionStore); return new DatasetResolverImpl(contractDefinitionResolver, assetIndex, policyDefinitionStore, distributionResolver, criterionOperatorRegistry); } + } diff --git a/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogDefaultServicesExtension.java b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogDefaultServicesExtension.java index 6bb270a167e..55f647c9b04 100644 --- a/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogDefaultServicesExtension.java +++ b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/CatalogDefaultServicesExtension.java @@ -52,4 +52,5 @@ public DataServiceRegistry dataServiceRegistry() { public DistributionResolver distributionResolver() { return new DefaultDistributionResolver(dataServiceRegistry, dataFlowManager); } + } diff --git a/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/ContractDefinitionResolverImpl.java b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/ContractDefinitionResolverImpl.java new file mode 100644 index 00000000000..6397a64d42c --- /dev/null +++ b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/ContractDefinitionResolverImpl.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2024 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.connector.controlplane.catalog; + +import org.eclipse.edc.connector.controlplane.catalog.spi.ContractDefinitionResolver; +import org.eclipse.edc.connector.controlplane.catalog.spi.ResolvedContractDefinitions; +import org.eclipse.edc.connector.controlplane.contract.spi.offer.store.ContractDefinitionStore; +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; +import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore; +import org.eclipse.edc.policy.engine.spi.PolicyContextImpl; +import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.agent.ParticipantAgent; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.Result; + +import java.util.HashMap; +import java.util.Optional; + +import static java.lang.String.format; +import static org.eclipse.edc.connector.controlplane.catalog.CatalogCoreExtension.CATALOG_SCOPE; + +/** + * Determines the contract definitions applicable to a {@link ParticipantAgent} by evaluating the access control and + * usage policies associated with a set of assets as defined by {@link ContractDefinition}s. On the distinction between + * access control and usage policy, see {@link ContractDefinition}. + */ +public class ContractDefinitionResolverImpl implements ContractDefinitionResolver { + private final PolicyEngine policyEngine; + private final PolicyDefinitionStore policyStore; + private final ContractDefinitionStore definitionStore; + + public ContractDefinitionResolverImpl(ContractDefinitionStore contractDefinitionStore, PolicyEngine policyEngine, PolicyDefinitionStore policyStore) { + definitionStore = contractDefinitionStore; + this.policyEngine = policyEngine; + this.policyStore = policyStore; + } + + @Override + public ResolvedContractDefinitions resolveFor(ParticipantAgent agent) { + var policies = new HashMap(); + var definitions = definitionStore.findAll(QuerySpec.max()) + .filter(definition -> { + var policyContext = PolicyContextImpl.Builder.newInstance().additional(ParticipantAgent.class, agent).build(); + var accessResult = Optional.of(definition.getAccessPolicyId()) + .map(policyId -> policies.computeIfAbsent(policyId, + key -> Optional.ofNullable(policyStore.findById(key)) + .map(PolicyDefinition::getPolicy) + .orElse(null)) + ) + .map(policy -> policyEngine.evaluate(CATALOG_SCOPE, policy, policyContext)) + .orElse(Result.failure(format("Policy %s not found", definition.getAccessPolicyId()))); + + return accessResult.succeeded(); + }) + .toList(); + + return new ResolvedContractDefinitions(definitions, policies); + } + +} diff --git a/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImpl.java b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImpl.java index 847fd3544b8..d9931825183 100644 --- a/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImpl.java +++ b/core/control-plane/control-plane-catalog/src/main/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImpl.java @@ -17,15 +17,17 @@ import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; import org.eclipse.edc.connector.controlplane.asset.spi.index.AssetIndex; import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; +import org.eclipse.edc.connector.controlplane.catalog.spi.ContractDefinitionResolver; import org.eclipse.edc.connector.controlplane.catalog.spi.DataService; import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; import org.eclipse.edc.connector.controlplane.catalog.spi.DatasetResolver; import org.eclipse.edc.connector.controlplane.catalog.spi.DistributionResolver; import org.eclipse.edc.connector.controlplane.contract.spi.ContractOfferId; -import org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver; import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore; import org.eclipse.edc.dataaddress.httpdata.spi.HttpDataAddressSchema; +import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.policy.model.PolicyType; import org.eclipse.edc.spi.agent.ParticipantAgent; import org.eclipse.edc.spi.query.CriterionOperatorRegistry; @@ -34,6 +36,7 @@ import java.util.Base64; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Predicate; import java.util.stream.Stream; @@ -61,14 +64,15 @@ public DatasetResolverImpl(ContractDefinitionResolver contractDefinitionResolver @Override @NotNull public Stream query(ParticipantAgent agent, QuerySpec querySpec) { - var contractDefinitions = contractDefinitionResolver.definitionsFor(agent).toList(); + var resolved = contractDefinitionResolver.resolveFor(agent); + var contractDefinitions = resolved.contractDefinitions(); if (contractDefinitions.isEmpty()) { return Stream.empty(); } var assetsQuery = QuerySpec.Builder.newInstance().offset(0).limit(MAX_VALUE).filter(querySpec.getFilterExpression()).build(); return assetIndex.queryAssets(assetsQuery) - .map(asset -> toDataset(contractDefinitions, asset)) + .map(asset -> toDataset(contractDefinitions, asset, resolved.policies())) .filter(Dataset::hasOffers) .skip(querySpec.getOffset()) .limit(querySpec.getLimit()); @@ -76,19 +80,20 @@ public Stream query(ParticipantAgent agent, QuerySpec querySpec) { @Override public Dataset getById(ParticipantAgent agent, String id) { - var contractDefinitions = contractDefinitionResolver.definitionsFor(agent).toList(); + var resolved = contractDefinitionResolver.resolveFor(agent); + var contractDefinitions = resolved.contractDefinitions(); if (contractDefinitions.isEmpty()) { return null; } return Optional.of(id) .map(assetIndex::findById) - .map(asset -> toDataset(contractDefinitions, asset)) + .map(asset -> toDataset(contractDefinitions, asset, resolved.policies())) .filter(Dataset::hasOffers) .orElse(null); } - private Dataset.Builder buildDataset(Asset asset) { + private Dataset.Builder buildDataset(Asset asset) { if (!asset.isCatalog()) { return Dataset.Builder.newInstance(); } @@ -101,7 +106,7 @@ private Dataset.Builder buildDataset(Asset asset) { .build()); } - private Dataset toDataset(List contractDefinitions, Asset asset) { + private Dataset toDataset(List contractDefinitions, Asset asset, Map policies) { var distributions = distributionResolver.getDistributions(asset); var datasetBuilder = buildDataset(asset) @@ -116,10 +121,15 @@ private Dataset toDataset(List contractDefinitions, Asset as .test(asset) ) .forEach(contractDefinition -> { - var policyDefinition = policyDefinitionStore.findById(contractDefinition.getContractPolicyId()); - if (policyDefinition != null) { + var policy = policies.computeIfAbsent(contractDefinition.getContractPolicyId(), policyId -> + Optional.ofNullable(policyDefinitionStore.findById(policyId)) + .map(PolicyDefinition::getPolicy) + .orElse(null) + ); + + if (policy != null) { var contractId = ContractOfferId.create(contractDefinition.getId(), asset.getId()); - var offerPolicy = policyDefinition.getPolicy().toBuilder().type(PolicyType.OFFER).build(); + var offerPolicy = policy.toBuilder().type(PolicyType.OFFER).build(); datasetBuilder.offer(contractId.toString(), offerPolicy); } }); diff --git a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer/ContractDefinitionResolverImplTest.java b/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/ContractDefinitionResolverImplTest.java similarity index 58% rename from core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer/ContractDefinitionResolverImplTest.java rename to core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/ContractDefinitionResolverImplTest.java index 33c7d7e3d35..320e094f036 100644 --- a/core/control-plane/control-plane-contract/src/test/java/org/eclipse/edc/connector/controlplane/contract/offer/ContractDefinitionResolverImplTest.java +++ b/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/ContractDefinitionResolverImplTest.java @@ -13,7 +13,7 @@ * */ -package org.eclipse.edc.connector.controlplane.contract.offer; +package org.eclipse.edc.connector.controlplane.catalog; import org.eclipse.edc.connector.controlplane.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; @@ -23,17 +23,16 @@ import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.policy.model.Policy; import org.eclipse.edc.spi.agent.ParticipantAgent; -import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.result.Result; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.Map; import java.util.stream.Stream; +import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver.CATALOGING_SCOPE; +import static org.assertj.core.api.Assertions.entry; +import static org.eclipse.edc.connector.controlplane.catalog.CatalogCoreExtension.CATALOG_SCOPE; import static org.mockito.AdditionalMatchers.and; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; @@ -41,36 +40,34 @@ import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.only; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; class ContractDefinitionResolverImplTest { - private final PolicyEngine policyEngine = mock(PolicyEngine.class); - private final PolicyDefinitionStore policyStore = mock(PolicyDefinitionStore.class); - private final ContractDefinitionStore definitionStore = mock(ContractDefinitionStore.class); + private final PolicyEngine policyEngine = mock(); + private final PolicyDefinitionStore policyStore = mock(); + private final ContractDefinitionStore definitionStore = mock(); - private ContractDefinitionResolverImpl definitionService; - - @BeforeEach - void setUp() { - definitionService = new ContractDefinitionResolverImpl(mock(Monitor.class), definitionStore, policyEngine, policyStore); - } + private final ContractDefinitionResolverImpl resolver = new ContractDefinitionResolverImpl(definitionStore, + policyEngine, policyStore); @Test - void definitionsFor_verifySatisfiesPolicies() { - var agent = new ParticipantAgent(Map.of(), Map.of()); + void shouldReturnDefinition_whenAccessPolicySatisfied() { + var agent = new ParticipantAgent(emptyMap(), emptyMap()); var def = PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).build(); when(policyStore.findById(any())).thenReturn(def); when(policyEngine.evaluate(any(), any(), isA(PolicyContext.class))).thenReturn(Result.success()); when(definitionStore.findAll(any())).thenReturn(Stream.of(createContractDefinition())); - var definitions = definitionService.definitionsFor(agent); + var result = resolver.resolveFor(agent); - assertThat(definitions).hasSize(1); + assertThat(result.contractDefinitions()).hasSize(1); + assertThat(result.policies()).hasSize(1); verify(policyEngine, atLeastOnce()).evaluate( - eq(CATALOGING_SCOPE), + eq(CATALOG_SCOPE), eq(def.getPolicy()), and(isA(PolicyContext.class), argThat(c -> c.getContextData(ParticipantAgent.class).equals(agent))) ); @@ -78,68 +75,61 @@ void definitionsFor_verifySatisfiesPolicies() { } @Test - void definitionsFor_verifyDoesNotSatisfyAccessPolicy() { - var agent = new ParticipantAgent(Map.of(), Map.of()); + void shouldNotReturnDefinition_whenAccessPolicyNotSatisfied() { + var agent = new ParticipantAgent(emptyMap(), emptyMap()); var definition = PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).id("access").build(); when(policyStore.findById(any())).thenReturn(definition); var contractDefinition = createContractDefinition(); when(policyEngine.evaluate(any(), any(), isA(PolicyContext.class))).thenReturn(Result.failure("invalid")); when(definitionStore.findAll(any())).thenReturn(Stream.of(contractDefinition)); - var result = definitionService.definitionsFor(agent); + var result = resolver.resolveFor(agent); - assertThat(result).isEmpty(); + assertThat(result.contractDefinitions()).isEmpty(); + assertThat(result.policies()).hasSize(1); verify(definitionStore).findAll(any()); } @Test - void definitionsFor_verifyPoliciesNotFound() { - var agent = new ParticipantAgent(Map.of(), Map.of()); + void shouldNotReturnDefinition_whenAccessPolicyDoesNotExist() { + var agent = new ParticipantAgent(emptyMap(), emptyMap()); when(policyStore.findById(any())).thenReturn(null); when(policyEngine.evaluate(any(), any(), isA(PolicyContext.class))).thenReturn(Result.success()); when(definitionStore.findAll(QuerySpec.max())).thenReturn(Stream.of(createContractDefinition())); - var definitions = definitionService.definitionsFor(agent); + var result = resolver.resolveFor(agent); - assertThat(definitions).isEmpty(); + assertThat(result.contractDefinitions()).isEmpty(); + assertThat(result.policies()).isEmpty(); verifyNoInteractions(policyEngine); } @Test - void definitionFor_found() { - var agent = new ParticipantAgent(Map.of(), Map.of()); - var definition = PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).build(); - var contractDefinition = createContractDefinition(); - when(policyStore.findById(any())).thenReturn(definition); + void shouldQueryPolicyOnce_whenDifferentDefinitionsHaveSamePolicy() { + var contractDefinition1 = contractDefinitionBuilder().accessPolicyId("accessPolicyId").build(); + var contractDefinition2 = contractDefinitionBuilder().accessPolicyId("accessPolicyId").build(); + var policy = Policy.Builder.newInstance().build(); + var policyDefinition = PolicyDefinition.Builder.newInstance().policy(policy).build(); + when(policyStore.findById(any())).thenReturn(policyDefinition); when(policyEngine.evaluate(any(), any(), isA(PolicyContext.class))).thenReturn(Result.success()); - when(definitionStore.findById("1")).thenReturn(contractDefinition); + when(definitionStore.findAll(any())).thenReturn(Stream.of(contractDefinition1, contractDefinition2)); - var result = definitionService.definitionFor(agent, "1"); + var result = resolver.resolveFor(new ParticipantAgent(emptyMap(), emptyMap())); - assertThat(result).isNotNull(); - verify(policyEngine, atLeastOnce()).evaluate( - eq(CATALOGING_SCOPE), - eq(definition.getPolicy()), - and(isA(PolicyContext.class), argThat(c -> c.getContextData(ParticipantAgent.class).equals(agent))) - ); + assertThat(result.contractDefinitions()).hasSize(2); + assertThat(result.policies()).hasSize(1).containsOnly(entry("accessPolicyId", policy)); + verify(policyStore, only()).findById("accessPolicyId"); } - @Test - void definitionFor_notFound() { - var agent = new ParticipantAgent(Map.of(), Map.of()); - when(definitionStore.findById(any())).thenReturn(null); - - var result = definitionService.definitionFor(agent, "nodefinition"); - - assertThat(result).isNull(); - verifyNoInteractions(policyEngine); + private ContractDefinition createContractDefinition() { + return contractDefinitionBuilder() + .build(); } - private ContractDefinition createContractDefinition() { + private ContractDefinition.Builder contractDefinitionBuilder() { return ContractDefinition.Builder.newInstance() .id("1") .accessPolicyId("access") - .contractPolicyId("contract") - .build(); + .contractPolicyId("contract"); } } diff --git a/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplIntegrationTest.java b/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplIntegrationTest.java index b64ef8d1570..6c323f2bdbc 100644 --- a/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplIntegrationTest.java +++ b/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplIntegrationTest.java @@ -16,8 +16,9 @@ import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; import org.eclipse.edc.connector.controlplane.asset.spi.index.AssetIndex; +import org.eclipse.edc.connector.controlplane.catalog.spi.ContractDefinitionResolver; import org.eclipse.edc.connector.controlplane.catalog.spi.DatasetResolver; -import org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver; +import org.eclipse.edc.connector.controlplane.catalog.spi.ResolvedContractDefinitions; import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; import org.eclipse.edc.connector.controlplane.defaults.storage.assetindex.InMemoryAssetIndex; import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; @@ -28,7 +29,6 @@ import org.eclipse.edc.spi.agent.ParticipantAgent; import org.eclipse.edc.spi.message.Range; import org.eclipse.edc.spi.query.Criterion; -import org.eclipse.edc.spi.query.CriterionOperatorRegistry; import org.eclipse.edc.spi.query.QuerySpec; import org.eclipse.edc.spi.types.domain.DataAddress; import org.jetbrains.annotations.NotNull; @@ -61,16 +61,15 @@ */ class DatasetResolverImplIntegrationTest { - private ContractDefinitionResolver contractDefinitionResolver; + private final ContractDefinitionResolver contractDefinitionResolver = mock(); private AssetIndex assetIndex; private DatasetResolver resolver; @BeforeEach void setUp() { - contractDefinitionResolver = mock(); PolicyDefinitionStore policyStore = mock(); - CriterionOperatorRegistry criterionOperatorRegistry = CriterionOperatorRegistryImpl.ofDefaults(); + var criterionOperatorRegistry = CriterionOperatorRegistryImpl.ofDefaults(); criterionOperatorRegistry.registerPropertyLookup(new AssetPropertyLookup()); assetIndex = new InMemoryAssetIndex(criterionOperatorRegistry); resolver = new DatasetResolverImpl( @@ -97,7 +96,7 @@ void shouldLimitResult_withHeterogeneousChunks() { var def2 = getContractDefBuilder("def2").assetsSelector(selectorFrom(assets2)).build(); var def3 = getContractDefBuilder("def3").assetsSelector(selectorFrom(assets3)).build(); - when(contractDefinitionResolver.definitionsFor(isA(ParticipantAgent.class))).thenAnswer(i -> Stream.of(def1, def2, def3)); + when(contractDefinitionResolver.resolveFor(isA(ParticipantAgent.class))).thenReturn(new ResolvedContractDefinitions(List.of(def1, def2, def3))); var from = 20; var to = 50; @@ -124,7 +123,7 @@ void should_return_offers_subset_when_across_multiple_contract_definitions(int f var contractDefinition2 = getContractDefBuilder("contract-definition-") .assetsSelector(selectorFrom(assets2)).build(); - when(contractDefinitionResolver.definitionsFor(isA(ParticipantAgent.class))).thenAnswer(i -> Stream.of(contractDefinition1, contractDefinition2)); + when(contractDefinitionResolver.resolveFor(isA(ParticipantAgent.class))).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition1, contractDefinition2))); var querySpec = QuerySpec.Builder.newInstance().range(new Range(from, to)).build(); var datasets = resolver.query(createAgent(), querySpec); @@ -143,7 +142,7 @@ void shouldLimitResult_insufficientAssets() { var def1 = getContractDefBuilder("def1").assetsSelector(selectorFrom(assets1)).build(); var def2 = getContractDefBuilder("def2").assetsSelector(selectorFrom(assets2)).build(); - when(contractDefinitionResolver.definitionsFor(isA(ParticipantAgent.class))).thenAnswer(i -> Stream.of(def1, def2)); + when(contractDefinitionResolver.resolveFor(isA(ParticipantAgent.class))).thenReturn(new ResolvedContractDefinitions(List.of(def1, def2))); var from = 14; var to = 50; @@ -158,7 +157,7 @@ void shouldLimitResult_insufficientAssets() { @Test void shouldLimitResult_pageOffsetLargerThanNumAssets() { var contractDefinition = range(0, 2).mapToObj(i -> getContractDefBuilder(String.valueOf(i)).build()); - when(contractDefinitionResolver.definitionsFor(isA(ParticipantAgent.class))).thenAnswer(i -> contractDefinition); + when(contractDefinitionResolver.resolveFor(isA(ParticipantAgent.class))).thenReturn(new ResolvedContractDefinitions(contractDefinition.toList())); var from = 25; var to = 50; diff --git a/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplTest.java b/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplTest.java index 9eba1ddc6df..afd546008fe 100644 --- a/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplTest.java +++ b/core/control-plane/control-plane-catalog/src/test/java/org/eclipse/edc/connector/controlplane/catalog/DatasetResolverImplTest.java @@ -19,13 +19,14 @@ import org.eclipse.edc.connector.controlplane.asset.spi.domain.Asset; import org.eclipse.edc.connector.controlplane.asset.spi.index.AssetIndex; import org.eclipse.edc.connector.controlplane.catalog.spi.Catalog; +import org.eclipse.edc.connector.controlplane.catalog.spi.ContractDefinitionResolver; import org.eclipse.edc.connector.controlplane.catalog.spi.DataService; import org.eclipse.edc.connector.controlplane.catalog.spi.Dataset; import org.eclipse.edc.connector.controlplane.catalog.spi.DatasetResolver; import org.eclipse.edc.connector.controlplane.catalog.spi.Distribution; import org.eclipse.edc.connector.controlplane.catalog.spi.DistributionResolver; +import org.eclipse.edc.connector.controlplane.catalog.spi.ResolvedContractDefinitions; import org.eclipse.edc.connector.controlplane.contract.spi.ContractOfferId; -import org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver; import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore; @@ -39,11 +40,15 @@ import org.eclipse.edc.spi.types.domain.DataAddress; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Stream; +import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.stream.IntStream.range; import static org.assertj.core.api.Assertions.assertThat; @@ -62,7 +67,7 @@ class DatasetResolverImplTest { - private final ContractDefinitionResolver contractDefinitionResolver = mock(ContractDefinitionResolver.class); + private final ContractDefinitionResolver definitionResolver = mock(ContractDefinitionResolver.class); private final AssetIndex assetIndex = mock(AssetIndex.class); private final PolicyDefinitionStore policyStore = mock(PolicyDefinitionStore.class); private final DistributionResolver distributionResolver = mock(DistributionResolver.class); @@ -71,273 +76,293 @@ class DatasetResolverImplTest { @BeforeEach void setUp() { - datasetResolver = new DatasetResolverImpl(contractDefinitionResolver, assetIndex, policyStore, distributionResolver, + datasetResolver = new DatasetResolverImpl(definitionResolver, assetIndex, policyStore, distributionResolver, CriterionOperatorRegistryImpl.ofDefaults()); } - @Test - void search_shouldReturnOneDatasetPerAsset() { - var dataService = createDataService(); - var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); - var contractPolicy = Policy.Builder.newInstance().build(); - var distribution = Distribution.Builder.newInstance().dataService(dataService).format("format").build(); - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of(contractDefinition)); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("assetId").property("key", "value").build())); - when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); - when(distributionResolver.getDistributions(isA(Asset.class))).thenReturn(List.of(distribution)); - - var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); - - assertThat(datasets).isNotNull().hasSize(1).first().satisfies(dataset -> { - assertThat(dataset.getId()).isEqualTo("assetId"); - assertThat(dataset.getDistributions()).hasSize(1).first().isEqualTo(distribution); - assertThat(dataset.getOffers()).hasSize(1).allSatisfy((id, policy) -> { - assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definitionId"); - assertThat(policy.getType()).isEqualTo(OFFER); - assertThat(policy.getTarget()).isEqualTo(null); + @Nested + class Query { + @Test + void shouldReturnOneDatasetPerAsset() { + var dataService = createDataService(); + var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); + var contractPolicy = Policy.Builder.newInstance().build(); + var distribution = Distribution.Builder.newInstance().dataService(dataService).format("format").build(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("assetId").property("key", "value").build())); + when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); + when(distributionResolver.getDistributions(isA(Asset.class))).thenReturn(List.of(distribution)); + + var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); + + assertThat(datasets).isNotNull().hasSize(1).first().satisfies(dataset -> { + assertThat(dataset.getId()).isEqualTo("assetId"); + assertThat(dataset.getDistributions()).hasSize(1).first().isEqualTo(distribution); + assertThat(dataset.getOffers()).hasSize(1).allSatisfy((id, policy) -> { + assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definitionId"); + assertThat(policy.getType()).isEqualTo(OFFER); + assertThat(policy.getTarget()).isEqualTo(null); + }); + assertThat(dataset.getProperties()).contains(entry("key", "value")); }); - assertThat(dataset.getProperties()).contains(entry("key", "value")); - }); - } - - @Test - void query_shouldNotQueryAssets_whenNoValidContractDefinition() { - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.empty()); - - var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); - - assertThat(datasets).isNotNull().isEmpty(); - verify(assetIndex, never()).queryAssets(any()); - } - - @Test - void query_shouldReturnNoDataset_whenPolicyNotFound() { - var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of(contractDefinition)); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("id").build())); - when(policyStore.findById("contractPolicyId")).thenReturn(null); - - var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); - - assertThat(datasets).isNotNull().isEmpty(); + } + + @Test + void shouldNotQueryAssets_whenNoValidContractDefinition() { + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(emptyList())); + + var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); + + assertThat(datasets).isNotNull().isEmpty(); + verify(assetIndex, never()).queryAssets(any()); + } + + @Test + void shouldReturnNoDataset_whenPolicyNotFound() { + var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("id").build())); + when(policyStore.findById("contractPolicyId")).thenReturn(null); + + var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); + + assertThat(datasets).isNotNull().isEmpty(); + } + + @Test + void shouldReturnOneDataset_whenMultipleDefinitionsOnSameAsset() { + var policy1 = Policy.Builder.newInstance().inheritsFrom("inherits1").build(); + var policy2 = Policy.Builder.newInstance().inheritsFrom("inherits2").build(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of( + contractDefinitionBuilder("definition1").contractPolicyId("policy1").build(), + contractDefinitionBuilder("definition2").contractPolicyId("policy2").build() + ))); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> Stream.of(createAsset("assetId").build())); + when(policyStore.findById("policy1")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy1).build()); + when(policyStore.findById("policy2")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy2).build()); + + var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); + + assertThat(datasets).hasSize(1).first().satisfies(dataset -> { + assertThat(dataset.getId()).isEqualTo("assetId"); + assertThat(dataset.getOffers()).hasSize(2) + .anySatisfy((id, policy) -> { + assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definition1"); + assertThat(policy.getInheritsFrom()).isEqualTo("inherits1"); + }) + .anySatisfy((id, policy) -> { + assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definition2"); + assertThat(policy.getInheritsFrom()).isEqualTo("inherits2"); + }); + }); + } + + @Test + void shouldFilterAssetsByPassedCriteria() { + var definitionCriterion = new Criterion(EDC_NAMESPACE + "id", "=", "id"); + var contractDefinition = contractDefinitionBuilder("definitionId") + .assetsSelector(List.of(definitionCriterion)) + .contractPolicyId("contractPolicyId") + .build(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("id").property("key", "value").build())); + when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).build()); + var additionalCriterion = new Criterion(EDC_NAMESPACE + "key", "=", "value"); + var querySpec = QuerySpec.Builder.newInstance().filter(additionalCriterion).build(); + + datasetResolver.query(createParticipantAgent(), querySpec); + + verify(assetIndex).queryAssets(and( + isA(QuerySpec.class), + argThat(q -> q.getFilterExpression().contains(additionalCriterion)) + )); + } + + @Test + void shouldLimitDataset_whenSingleDefinitionAndMultipleAssets_contained() { + var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); + var contractPolicy = Policy.Builder.newInstance().build(); + var assets = range(0, 10).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); + when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); + var querySpec = QuerySpec.Builder.newInstance().range(new Range(2, 5)).build(); + + var datasets = datasetResolver.query(createParticipantAgent(), querySpec); + + assertThat(datasets).hasSize(3).map(getId()).containsExactly("2", "3", "4"); + } + + @Test + void shouldLimitDataset_whenSingleDefinitionAndMultipleAssets_overflowing() { + var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); + var contractPolicy = Policy.Builder.newInstance().build(); + var assets = range(0, 10).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); + when(policyStore.findById(any())).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); + var querySpec = QuerySpec.Builder.newInstance().range(new Range(7, 15)).build(); + + var datasets = datasetResolver.query(createParticipantAgent(), querySpec); + + assertThat(datasets).hasSize(3).map(getId()).containsExactly("7", "8", "9"); + } + + @Test + void shouldLimitDataset_whenMultipleDefinitionAndMultipleAssets_across() { + var contractDefinitions = range(0, 2).mapToObj(it -> contractDefinitionBuilder(String.valueOf(it)).build()).toList(); + var contractPolicy = Policy.Builder.newInstance().build(); + var assets = range(0, 20).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(contractDefinitions)); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); + when(policyStore.findById(any())).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); + var querySpec = QuerySpec.Builder.newInstance().range(new Range(6, 14)).build(); + + var datasets = datasetResolver.query(createParticipantAgent(), querySpec); + + assertThat(datasets).hasSize(8).map(getId()).containsExactly("6", "7", "8", "9", "10", "11", "12", "13"); + } + + @Test + void shouldLimitDataset_whenMultipleDefinitionsWithSameAssets() { + var contractDefinitions = range(0, 2).mapToObj(it -> contractDefinitionBuilder(String.valueOf(it)).build()).toList(); + var contractPolicy = Policy.Builder.newInstance().build(); + var assets = range(0, 10).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(contractDefinitions)); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); + when(policyStore.findById(any())).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); + var querySpec = QuerySpec.Builder.newInstance().range(new Range(6, 8)).build(); + + var datasets = datasetResolver.query(createParticipantAgent(), querySpec); + + assertThat(datasets).hasSize(2) + .allSatisfy(dataset -> assertThat(dataset.getOffers()).hasSize(2)) + .map(getId()).containsExactly("6", "7"); + } + + @Test + void shouldReturnCatalogWithinCatalog_whenAssetIsCatalogAsset() { + var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); + var contractPolicy = Policy.Builder.newInstance().build(); + var distribution = Distribution.Builder.newInstance().dataService(DataService.Builder.newInstance() + .endpointDescription("test-asset-desc") + .endpointUrl("https://foo.bar/baz") + .build()) + .format(HttpDataAddressSchema.HTTP_DATA_TYPE).build(); + + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("assetId") + .property(Asset.PROPERTY_IS_CATALOG, true) + .dataAddress(DataAddress.Builder.newInstance().type(HttpDataAddressSchema.HTTP_DATA_TYPE).build()) + .build())); + when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); + when(distributionResolver.getDistributions(isA(Asset.class))).thenReturn(List.of(distribution)); + + var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); + + assertThat(datasets).isNotNull().hasSize(1).first().satisfies(dataset -> { + assertThat(dataset).isInstanceOf(Catalog.class); + assertThat(dataset.getId()).isEqualTo("assetId"); + assertThat(dataset.getOffers()).hasSize(1).allSatisfy((id, policy) -> { + assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definitionId"); + assertThat(policy.getType()).isEqualTo(OFFER); + assertThat(policy.getTarget()).isEqualTo(null); + }); + }); + } + + @Test + void shouldNotFetchContractPolicy_whenIsSameAsAccessPolicy() { + var dataService = createDataService(); + var contractDefinition = contractDefinitionBuilder("definitionId").accessPolicyId("samePolicy").contractPolicyId("samePolicy").build(); + var distribution = Distribution.Builder.newInstance().dataService(dataService).format("format").build(); + var cachedPolicies = new HashMap<>(Map.of("samePolicy", Policy.Builder.newInstance().build())); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition), cachedPolicies)); + when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("assetId").property("key", "value").build())); + when(distributionResolver.getDistributions(isA(Asset.class))).thenReturn(List.of(distribution)); + + var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); + + assertThat(datasets).hasSize(1); + verify(policyStore, never()).findById(any()); + } } - @Test - void query_shouldReturnOneDataset_whenMultipleDefinitionsOnSameAsset() { - var policy1 = Policy.Builder.newInstance().inheritsFrom("inherits1").build(); - var policy2 = Policy.Builder.newInstance().inheritsFrom("inherits2").build(); - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of( - contractDefinitionBuilder("definition1").contractPolicyId("policy1").build(), - contractDefinitionBuilder("definition2").contractPolicyId("policy2").build() - )); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> Stream.of(createAsset("assetId").build())); - when(policyStore.findById("policy1")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy1).build()); - when(policyStore.findById("policy2")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy2).build()); - - var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); - - assertThat(datasets).hasSize(1).first().satisfies(dataset -> { - assertThat(dataset.getId()).isEqualTo("assetId"); + @Nested + class GetById { + @Test + void shouldReturnDataset() { + var policy1 = Policy.Builder.newInstance().inheritsFrom("inherits1").build(); + var policy2 = Policy.Builder.newInstance().inheritsFrom("inherits2").build(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of( + contractDefinitionBuilder("definition1").contractPolicyId("policy1").build(), + contractDefinitionBuilder("definition2").contractPolicyId("policy2").build() + ))); + when(assetIndex.findById(any())).thenReturn(createAsset("datasetId").build()); + when(policyStore.findById("policy1")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy1).build()); + when(policyStore.findById("policy2")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy2).build()); + var participantAgent = createParticipantAgent(); + + var dataset = datasetResolver.getById(participantAgent, "datasetId"); + + assertThat(dataset).isNotNull(); + assertThat(dataset.getId()).isEqualTo("datasetId"); assertThat(dataset.getOffers()).hasSize(2) .anySatisfy((id, policy) -> { - assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definition1"); + assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).isEqualTo("definition1"); assertThat(policy.getInheritsFrom()).isEqualTo("inherits1"); }) .anySatisfy((id, policy) -> { - assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definition2"); + assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).isEqualTo("definition2"); assertThat(policy.getInheritsFrom()).isEqualTo("inherits2"); }); - }); - } - - @Test - void query_shouldFilterAssetsByPassedCriteria() { - var definitionCriterion = new Criterion(EDC_NAMESPACE + "id", "=", "id"); - var contractDefinition = contractDefinitionBuilder("definitionId") - .assetsSelector(List.of(definitionCriterion)) - .contractPolicyId("contractPolicyId") - .build(); - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of(contractDefinition)); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("id").property("key", "value").build())); - when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(Policy.Builder.newInstance().build()).build()); - var additionalCriterion = new Criterion(EDC_NAMESPACE + "key", "=", "value"); - var querySpec = QuerySpec.Builder.newInstance().filter(additionalCriterion).build(); - - datasetResolver.query(createParticipantAgent(), querySpec); - - verify(assetIndex).queryAssets(and( - isA(QuerySpec.class), - argThat(q -> q.getFilterExpression().contains(additionalCriterion)) - )); - } - - @Test - void query_shouldLimitDataset_whenSingleDefinitionAndMultipleAssets_contained() { - var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); - var contractPolicy = Policy.Builder.newInstance().build(); - var assets = range(0, 10).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of(contractDefinition)); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); - when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); - var querySpec = QuerySpec.Builder.newInstance().range(new Range(2, 5)).build(); + verify(assetIndex).findById("datasetId"); + verify(definitionResolver).resolveFor(participantAgent); + } - var datasets = datasetResolver.query(createParticipantAgent(), querySpec); + @Test + void shouldReturnNull_whenAssetNotFound() { + var contractDefinition = contractDefinitionBuilder("definition1").contractPolicyId("policy1").build(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.findById(any())).thenReturn(null); + var participantAgent = createParticipantAgent(); - assertThat(datasets).hasSize(3).map(getId()).containsExactly("2", "3", "4"); - } + var dataset = datasetResolver.getById(participantAgent, "datasetId"); - @Test - void query_shouldLimitDataset_whenSingleDefinitionAndMultipleAssets_overflowing() { - var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); - var contractPolicy = Policy.Builder.newInstance().build(); - var assets = range(0, 10).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of(contractDefinition)); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); - when(policyStore.findById(any())).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); - var querySpec = QuerySpec.Builder.newInstance().range(new Range(7, 15)).build(); + assertThat(dataset).isNull(); + } - var datasets = datasetResolver.query(createParticipantAgent(), querySpec); + @Test + void shouldReturnNull_whenNoValidContractDefinition() { + var participantAgent = createParticipantAgent(); - assertThat(datasets).hasSize(3).map(getId()).containsExactly("7", "8", "9"); - } + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(emptyList())); - @Test - void query_shouldLimitDataset_whenMultipleDefinitionAndMultipleAssets_across() { - var contractDefinitions = range(0, 2).mapToObj(it -> contractDefinitionBuilder(String.valueOf(it)).build()).toList(); - var contractPolicy = Policy.Builder.newInstance().build(); - var assets = range(0, 20).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); - when(contractDefinitionResolver.definitionsFor(any())).thenAnswer(it -> contractDefinitions.stream()); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); - when(policyStore.findById(any())).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); - var querySpec = QuerySpec.Builder.newInstance().range(new Range(6, 14)).build(); + var dataset = datasetResolver.getById(participantAgent, "datasetId"); - var datasets = datasetResolver.query(createParticipantAgent(), querySpec); + assertThat(dataset).isNull(); + verify(assetIndex, never()).findById(any()); + } - assertThat(datasets).hasSize(8).map(getId()).containsExactly("6", "7", "8", "9", "10", "11", "12", "13"); - } + @Test + void shouldReturnNull_whenNoValidContractDefinitionForAsset() { + var assetId = "assetId"; + var participantAgent = createParticipantAgent(); - @Test - void query_shouldLimitDataset_whenMultipleDefinitionsWithSameAssets() { - var contractDefinitions = range(0, 2).mapToObj(it -> contractDefinitionBuilder(String.valueOf(it)).build()).toList(); - var contractPolicy = Policy.Builder.newInstance().build(); - var assets = range(0, 10).mapToObj(it -> createAsset(String.valueOf(it)).build()).toList(); - when(contractDefinitionResolver.definitionsFor(any())).thenAnswer(it -> contractDefinitions.stream()); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenAnswer(i -> assets.stream()); - when(policyStore.findById(any())).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); - var querySpec = QuerySpec.Builder.newInstance().range(new Range(6, 8)).build(); - - var datasets = datasetResolver.query(createParticipantAgent(), querySpec); - - assertThat(datasets).hasSize(2) - .allSatisfy(dataset -> assertThat(dataset.getOffers()).hasSize(2)) - .map(getId()).containsExactly("6", "7"); - } + var contractDefinition = contractDefinitionBuilder("definition") + .assetsSelectorCriterion(Criterion.Builder.newInstance() + .operandRight(EDC_NAMESPACE + "id") + .operator("=") + .operandLeft("a-different-asset") + .build()) + .build(); + when(definitionResolver.resolveFor(any())).thenReturn(new ResolvedContractDefinitions(List.of(contractDefinition))); + when(assetIndex.findById(any())).thenReturn(createAsset(assetId).build()); - @Test - void query_shouldReturnCatalogWithinCatalog_whenAssetIsCatalogAsset() { - var contractDefinition = contractDefinitionBuilder("definitionId").contractPolicyId("contractPolicyId").build(); - var contractPolicy = Policy.Builder.newInstance().build(); - var distribution = Distribution.Builder.newInstance().dataService(DataService.Builder.newInstance() - .endpointDescription("test-asset-desc") - .endpointUrl("https://foo.bar/baz") - .build()) - .format(HttpDataAddressSchema.HTTP_DATA_TYPE).build(); - - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of(contractDefinition)); - when(assetIndex.queryAssets(isA(QuerySpec.class))).thenReturn(Stream.of(createAsset("assetId") - .property(Asset.PROPERTY_IS_CATALOG, true) - .dataAddress(DataAddress.Builder.newInstance().type(HttpDataAddressSchema.HTTP_DATA_TYPE).build()) - .build())); - when(policyStore.findById("contractPolicyId")).thenReturn(PolicyDefinition.Builder.newInstance().policy(contractPolicy).build()); - when(distributionResolver.getDistributions(isA(Asset.class))).thenReturn(List.of(distribution)); - - var datasets = datasetResolver.query(createParticipantAgent(), QuerySpec.none()); - - assertThat(datasets).isNotNull().hasSize(1).first().satisfies(dataset -> { - assertThat(dataset).isInstanceOf(Catalog.class); - assertThat(dataset.getId()).isEqualTo("assetId"); - assertThat(dataset.getOffers()).hasSize(1).allSatisfy((id, policy) -> { - assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).asString().isEqualTo("definitionId"); - assertThat(policy.getType()).isEqualTo(OFFER); - assertThat(policy.getTarget()).isEqualTo(null); - }); - }); - } + var dataset = datasetResolver.getById(participantAgent, assetId); - @Test - void getById_shouldReturnDataset() { - var policy1 = Policy.Builder.newInstance().inheritsFrom("inherits1").build(); - var policy2 = Policy.Builder.newInstance().inheritsFrom("inherits2").build(); - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of( - contractDefinitionBuilder("definition1").contractPolicyId("policy1").build(), - contractDefinitionBuilder("definition2").contractPolicyId("policy2").build() - )); - when(assetIndex.findById(any())).thenReturn(createAsset("datasetId").build()); - when(policyStore.findById("policy1")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy1).build()); - when(policyStore.findById("policy2")).thenReturn(PolicyDefinition.Builder.newInstance().policy(policy2).build()); - var participantAgent = createParticipantAgent(); - - var dataset = datasetResolver.getById(participantAgent, "datasetId"); - - assertThat(dataset).isNotNull(); - assertThat(dataset.getId()).isEqualTo("datasetId"); - assertThat(dataset.getOffers()).hasSize(2) - .anySatisfy((id, policy) -> { - assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).isEqualTo("definition1"); - assertThat(policy.getInheritsFrom()).isEqualTo("inherits1"); - }) - .anySatisfy((id, policy) -> { - assertThat(ContractOfferId.parseId(id)).isSucceeded().extracting(ContractOfferId::definitionPart).isEqualTo("definition2"); - assertThat(policy.getInheritsFrom()).isEqualTo("inherits2"); - }); - verify(assetIndex).findById("datasetId"); - verify(contractDefinitionResolver).definitionsFor(participantAgent); - } - - @Test - void getById_shouldReturnNull_whenAssetNotFound() { - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of( - contractDefinitionBuilder("definition1").contractPolicyId("policy1").build() - )); - when(assetIndex.findById(any())).thenReturn(null); - var participantAgent = createParticipantAgent(); - - var dataset = datasetResolver.getById(participantAgent, "datasetId"); - - assertThat(dataset).isNull(); - } - - @Test - void getById_shouldReturnNull_whenNoValidContractDefinition() { - var participantAgent = createParticipantAgent(); - - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.empty()); - - var dataset = datasetResolver.getById(participantAgent, "datasetId"); - - assertThat(dataset).isNull(); - verify(assetIndex, never()).findById(any()); - } - - @Test - void getById_shouldReturnNull_whenNoValidContractDefinitionForAsset() { - var assetId = "assetId"; - var participantAgent = createParticipantAgent(); - - when(contractDefinitionResolver.definitionsFor(any())).thenReturn(Stream.of( - contractDefinitionBuilder("definition") - .assetsSelectorCriterion(Criterion.Builder.newInstance() - .operandRight(EDC_NAMESPACE + "id") - .operator("=") - .operandLeft("a-different-asset") - .build()) - .build() - )); - when(assetIndex.findById(any())).thenReturn(createAsset(assetId).build()); - - var dataset = datasetResolver.getById(participantAgent, assetId); - - assertThat(dataset).isNull(); + assertThat(dataset).isNull(); + } } private ContractDefinition.Builder contractDefinitionBuilder(String id) { diff --git a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractCoreExtension.java b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractCoreExtension.java index 54dc97ae0d6..1a0415585d7 100644 --- a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractCoreExtension.java +++ b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractCoreExtension.java @@ -34,6 +34,7 @@ import org.eclipse.edc.connector.controlplane.policy.contract.ContractExpiryCheckFunction; import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore; import org.eclipse.edc.policy.engine.spi.PolicyEngine; +import org.eclipse.edc.policy.engine.spi.PolicyScope; import org.eclipse.edc.policy.engine.spi.RuleBindingRegistry; import org.eclipse.edc.policy.model.Permission; import org.eclipse.edc.runtime.metamodel.annotation.CoreExtension; @@ -95,6 +96,9 @@ public class ContractCoreExtension implements ServiceExtension { @Setting(value = "The base delay for the provider negotiation retry mechanism in millisecond", type = "long", defaultValue = DEFAULT_SEND_RETRY_BASE_DELAY + "") private static final String NEGOTIATION_PROVIDER_SEND_RETRY_BASE_DELAY_MS = "edc.negotiation.provider.send.retry.base-delay.ms"; + @PolicyScope + public static final String CATALOG_SCOPE = "catalog"; + private ConsumerContractNegotiationManagerImpl consumerNegotiationManager; private ProviderContractNegotiationManagerImpl providerNegotiationManager; diff --git a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractNegotiationDefaultServicesExtension.java b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractNegotiationDefaultServicesExtension.java index c93a1e4d22c..a6e2ac91574 100644 --- a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractNegotiationDefaultServicesExtension.java +++ b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/ContractNegotiationDefaultServicesExtension.java @@ -16,22 +16,18 @@ import org.eclipse.edc.connector.controlplane.contract.observe.ContractNegotiationObservableImpl; import org.eclipse.edc.connector.controlplane.contract.offer.ConsumerOfferResolverImpl; -import org.eclipse.edc.connector.controlplane.contract.offer.ContractDefinitionResolverImpl; import org.eclipse.edc.connector.controlplane.contract.policy.PolicyArchiveImpl; import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.ContractNegotiationPendingGuard; import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.observe.ContractNegotiationObservable; import org.eclipse.edc.connector.controlplane.contract.spi.negotiation.store.ContractNegotiationStore; import org.eclipse.edc.connector.controlplane.contract.spi.offer.ConsumerOfferResolver; -import org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver; import org.eclipse.edc.connector.controlplane.contract.spi.offer.store.ContractDefinitionStore; import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyArchive; import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore; -import org.eclipse.edc.policy.engine.spi.PolicyEngine; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; /** * Contract Negotiation Default Services Extension @@ -44,20 +40,12 @@ public class ContractNegotiationDefaultServicesExtension implements ServiceExten @Inject private ContractDefinitionStore contractDefinitionStore; - @Inject - private PolicyEngine policyEngine; - @Inject private PolicyDefinitionStore policyStore; @Inject private ContractNegotiationStore store; - @Provider - public ContractDefinitionResolver contractDefinitionResolver(ServiceExtensionContext context) { - return new ContractDefinitionResolverImpl(context.getMonitor(), contractDefinitionStore, policyEngine, policyStore); - } - @Provider public ConsumerOfferResolver consumerOfferResolver() { return new ConsumerOfferResolverImpl(contractDefinitionStore, policyStore); diff --git a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ContractDefinitionResolverImpl.java b/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ContractDefinitionResolverImpl.java deleted file mode 100644 index 0d821d94675..00000000000 --- a/core/control-plane/control-plane-contract/src/main/java/org/eclipse/edc/connector/controlplane/contract/offer/ContractDefinitionResolverImpl.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2021 Daimler TSS GmbH - * - * 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: - * Daimler TSS GmbH - Initial API and Implementation - * Microsoft Corporation - Refactoring - * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - improvements - * - */ - -package org.eclipse.edc.connector.controlplane.contract.offer; - -import org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver; -import org.eclipse.edc.connector.controlplane.contract.spi.offer.store.ContractDefinitionStore; -import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; -import org.eclipse.edc.connector.controlplane.policy.spi.PolicyDefinition; -import org.eclipse.edc.connector.controlplane.policy.spi.store.PolicyDefinitionStore; -import org.eclipse.edc.policy.engine.spi.PolicyContextImpl; -import org.eclipse.edc.policy.engine.spi.PolicyEngine; -import org.eclipse.edc.spi.agent.ParticipantAgent; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.query.QuerySpec; -import org.eclipse.edc.spi.result.Result; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Optional; -import java.util.stream.Stream; - -import static java.lang.String.format; - -/** - * Determines the contract definitions applicable to a {@link ParticipantAgent} by evaluating the access control and - * usage policies associated with a set of assets as defined by {@link ContractDefinition}s. On the distinction between - * access control and usage policy, see {@link ContractDefinition}. - */ -public class ContractDefinitionResolverImpl implements ContractDefinitionResolver { - private final PolicyEngine policyEngine; - private final PolicyDefinitionStore policyStore; - private final Monitor monitor; - private final ContractDefinitionStore definitionStore; - - public ContractDefinitionResolverImpl(Monitor monitor, ContractDefinitionStore contractDefinitionStore, PolicyEngine policyEngine, PolicyDefinitionStore policyStore) { - this.monitor = monitor; - definitionStore = contractDefinitionStore; - this.policyEngine = policyEngine; - this.policyStore = policyStore; - } - - @NotNull - @Override - public Stream definitionsFor(ParticipantAgent agent) { - return definitionStore.findAll(QuerySpec.max()) - .filter(definition -> evaluateAccessPolicy(definition, agent)); - } - - @Nullable - @Override - public ContractDefinition definitionFor(ParticipantAgent agent, String definitionId) { - return Optional.of(definitionId) - .map(definitionStore::findById) - .filter(definition -> evaluateAccessPolicy(definition, agent)) - .orElse(null); - } - - /** - * Determines the applicability of a definition to an agent by evaluating its access policy. - */ - private boolean evaluateAccessPolicy(ContractDefinition definition, ParticipantAgent agent) { - var policyContext = PolicyContextImpl.Builder.newInstance().additional(ParticipantAgent.class, agent).build(); - var accessResult = Optional.of(definition.getAccessPolicyId()) - .map(policyStore::findById) - .map(PolicyDefinition::getPolicy) - .map(policy -> policyEngine.evaluate(CATALOGING_SCOPE, policy, policyContext)) - .orElse(Result.failure(format("Policy %s not found", definition.getAccessPolicyId()))); - - if (accessResult.failed()) { - monitor.debug(format("Access not granted for %s: \n%s", definition.getId(), String.join("\n", accessResult.getFailureMessages()))); - return false; - } - - return true; - } -} 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 dafeeb71ceb..24ec40a1985 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 @@ -40,7 +40,7 @@ import java.util.Optional; import static java.lang.String.format; -import static org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver.CATALOGING_SCOPE; +import static org.eclipse.edc.connector.controlplane.contract.ContractCoreExtension.CATALOG_SCOPE; import static org.eclipse.edc.spi.result.Result.failure; import static org.eclipse.edc.spi.result.Result.success; @@ -128,7 +128,7 @@ private Result validateInitialOffer(ValidatableConsumerOffer consumerOff return failure("Invalid consumer identity"); } - var accessPolicyResult = evaluatePolicy(consumerOffer.getAccessPolicy(), CATALOGING_SCOPE, agent, consumerOffer.getOfferId()); + var accessPolicyResult = evaluatePolicy(consumerOffer.getAccessPolicy(), CATALOG_SCOPE, agent, consumerOffer.getOfferId()); if (accessPolicyResult.failed()) { return accessPolicyResult; 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 ad2c8578b4b..37bb04fea41 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 @@ -48,7 +48,7 @@ import static java.time.Instant.MIN; import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.edc.connector.controlplane.contract.spi.offer.ContractDefinitionResolver.CATALOGING_SCOPE; +import static org.eclipse.edc.connector.controlplane.contract.ContractCoreExtension.CATALOG_SCOPE; import static org.eclipse.edc.connector.controlplane.contract.spi.validation.ContractValidationService.NEGOTIATION_SCOPE; import static org.eclipse.edc.connector.controlplane.contract.spi.validation.ContractValidationService.TRANSFER_SCOPE; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; @@ -99,7 +99,7 @@ void verifyContractOfferValidation() { var asset = Asset.Builder.newInstance().id("1").build(); when(assetIndex.findById("1")).thenReturn(asset); - when(policyEngine.evaluate(eq(CATALOGING_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.success()); + when(policyEngine.evaluate(eq(CATALOG_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.success()); when(policyEngine.evaluate(eq(NEGOTIATION_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.success()); var validatableOffer = createValidatableConsumerOffer(asset, originalPolicy); @@ -114,7 +114,7 @@ void verifyContractOfferValidation() { verify(assetIndex).findById("1"); verify(policyEngine).evaluate( - eq(CATALOGING_SCOPE), + eq(CATALOG_SCOPE), eq(newPolicy), and(isA(PolicyContext.class), argThat(c -> c.getContextData(ParticipantAgent.class).equals(participantAgent))) ); @@ -326,7 +326,7 @@ void validateInitialOffer_assetInOfferNotReferencedByDefinition_shouldFail() { var validatableOffer = createValidatableConsumerOffer(); var participantAgent = new ParticipantAgent(emptyMap(), Map.of(PARTICIPANT_IDENTITY, CONSUMER_ID)); - when(policyEngine.evaluate(eq(CATALOGING_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.success()); + when(policyEngine.evaluate(eq(CATALOG_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.success()); when(assetIndex.findById(anyString())).thenReturn(Asset.Builder.newInstance().build()); when(assetIndex.countAssets(anyList())).thenReturn(0L); @@ -341,7 +341,7 @@ void validateInitialOffer_fails_whenContractPolicyEvaluationFails() { var validatableOffer = createValidatableConsumerOffer(); var participantAgent = new ParticipantAgent(emptyMap(), Map.of(PARTICIPANT_IDENTITY, CONSUMER_ID)); - when(policyEngine.evaluate(eq(CATALOGING_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.success()); + when(policyEngine.evaluate(eq(CATALOG_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.success()); when(policyEngine.evaluate(eq(NEGOTIATION_SCOPE), any(), isA(PolicyContext.class))).thenReturn(Result.failure("evaluation failure")); when(assetIndex.findById(anyString())).thenReturn(Asset.Builder.newInstance().build()); when(assetIndex.countAssets(anyList())).thenReturn(1L); diff --git a/spi/control-plane/catalog-spi/build.gradle.kts b/spi/control-plane/catalog-spi/build.gradle.kts index f929c2c8c24..546cbada839 100644 --- a/spi/control-plane/catalog-spi/build.gradle.kts +++ b/spi/control-plane/catalog-spi/build.gradle.kts @@ -19,7 +19,9 @@ plugins { dependencies { api(project(":spi:common:core-spi")) + api(project(":spi:common:policy-engine-spi")) api(project(":spi:control-plane:asset-spi")) + api(project(":spi:control-plane:contract-spi")) testImplementation(project(":core:common:lib:json-lib")) diff --git a/spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ContractDefinitionResolver.java b/spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ContractDefinitionResolver.java new file mode 100644 index 00000000000..641702f113a --- /dev/null +++ b/spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ContractDefinitionResolver.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.connector.controlplane.catalog.spi; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.agent.ParticipantAgent; + +/** + * Returns {@link ContractDefinition} for a given participant agent. + *

+ * A runtime extension may implement custom logic to determine which contract definitions are returned. + */ +@ExtensionPoint +public interface ContractDefinitionResolver { + + /** + * Returns definitions for the given participant given, plus the access policy objects related. + * + * @param agent the participant agent. + * @return resolved contract definitions. + */ + ResolvedContractDefinitions resolveFor(ParticipantAgent agent); +} diff --git a/spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ResolvedContractDefinitions.java b/spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ResolvedContractDefinitions.java new file mode 100644 index 00000000000..9b7a2769644 --- /dev/null +++ b/spi/control-plane/catalog-spi/src/main/java/org/eclipse/edc/connector/controlplane/catalog/spi/ResolvedContractDefinitions.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Cofinity-X + * + * 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: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.connector.controlplane.catalog.spi; + +import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; +import org.eclipse.edc.policy.model.Policy; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represent resolved contract definitions and cached policy objects by {@link ContractDefinitionResolver} + * + * @param contractDefinitions the contract definitions. + * @param policies the cached access policies. + */ +public record ResolvedContractDefinitions(List contractDefinitions, Map policies) { + + public ResolvedContractDefinitions(List contractDefinitions) { + this(contractDefinitions, new HashMap<>()); + } +} diff --git a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/offer/ContractDefinitionResolver.java b/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/offer/ContractDefinitionResolver.java deleted file mode 100644 index 9480e9c1002..00000000000 --- a/spi/control-plane/contract-spi/src/main/java/org/eclipse/edc/connector/controlplane/contract/spi/offer/ContractDefinitionResolver.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2021 Daimler TSS GmbH - * - * 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: - * Daimler TSS GmbH - Initial API and Implementation - * - */ - -package org.eclipse.edc.connector.controlplane.contract.spi.offer; - -import org.eclipse.edc.connector.controlplane.contract.spi.types.offer.ContractDefinition; -import org.eclipse.edc.policy.engine.spi.PolicyScope; -import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; -import org.eclipse.edc.spi.agent.ParticipantAgent; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.stream.Stream; - -/** - * Returns {@link ContractDefinition} for a given participant agent. - *

- * A runtime extension may implement custom logic to determine which contract definitions are returned. - */ -@ExtensionPoint -public interface ContractDefinitionResolver { - - @PolicyScope - String CATALOGING_SCOPE = "catalog"; - - /** - * Returns the definitions for the given participant agent. - */ - @NotNull - Stream definitionsFor(ParticipantAgent agent); - - /** - * Returns a contract definition for the agent associated with the given contract definition id. If the definition - * does not exist or the agent is not authorized, the result will indicate the request is invalid. - */ - @Nullable - ContractDefinition definitionFor(ParticipantAgent agent, String definitionId); -}