diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/build.gradle.kts b/extensions/common/iam/decentralized-claims/decentralized-claims-core/build.gradle.kts index 3dbac6b18f..fbef01bd8b 100644 --- a/extensions/common/iam/decentralized-claims/decentralized-claims-core/build.gradle.kts +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/build.gradle.kts @@ -6,6 +6,10 @@ plugins { dependencies { api(project(":spi:common:decentralized-claims-spi")) api(project(":spi:common:policy:request-policy-context-spi")) + api(project(":spi:common:transaction-spi")) + api(project(":spi:control-plane:catalog-spi")) + api(project(":spi:control-plane:contract-spi")) + api(project(":spi:control-plane:transfer-spi")) implementation(project(":spi:common:json-ld-spi")) implementation(project(":spi:common:http-spi")) implementation(project(":spi:common:keys-spi")) @@ -15,6 +19,7 @@ dependencies { implementation(project(":core:common:lib:util-lib")) implementation(project(":core:common:lib:crypto-common-lib")) implementation(project(":core:common:lib:token-lib")) + implementation(project(":core:common:lib:store-lib")) implementation(project(":extensions:common:crypto:lib:jws2020-lib")) implementation(project(":extensions:common:crypto:jwt-verifiable-credentials")) implementation(project(":extensions:common:crypto:ldp-verifiable-credentials")) @@ -29,6 +34,7 @@ dependencies { testImplementation(testFixtures(project(":spi:common:decentralized-claims-spi"))) testImplementation(testFixtures(project(":spi:common:verifiable-credentials-spi"))) testImplementation(project(":core:common:lib:json-ld-lib")) + testImplementation(project(":core:common:lib:query-lib")) testImplementation(project(":extensions:common:json-ld")) testImplementation(libs.nimbus.jwt) testImplementation(libs.awaitility) diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DcpDefaultServicesExtension.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DcpDefaultServicesExtension.java index 68eae4e819..db40f0f471 100644 --- a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DcpDefaultServicesExtension.java +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DcpDefaultServicesExtension.java @@ -19,8 +19,10 @@ import org.eclipse.edc.iam.decentralizedclaims.core.defaults.DefaultTrustedIssuerRegistry; import org.eclipse.edc.iam.decentralizedclaims.core.defaults.InMemorySignatureSuiteRegistry; import org.eclipse.edc.iam.decentralizedclaims.core.scope.DcpScopeExtractorRegistry; +import org.eclipse.edc.iam.decentralizedclaims.core.scope.defaults.InMemoryDcpScopeStore; import org.eclipse.edc.iam.decentralizedclaims.spi.ClaimTokenCreatorFunction; import org.eclipse.edc.iam.decentralizedclaims.spi.scope.ScopeExtractorRegistry; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.store.DcpScopeStore; import org.eclipse.edc.iam.decentralizedclaims.spi.verification.SignatureSuiteRegistry; import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry; import org.eclipse.edc.jwt.signer.spi.JwsSignerProvider; @@ -32,6 +34,7 @@ import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.iam.AudienceResolver; import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.query.CriterionOperatorRegistry; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.system.ServiceExtension; @@ -62,6 +65,8 @@ public class DcpDefaultServicesExtension implements ServiceExtension { private JwsSignerProvider externalSigner; @Inject private JtiValidationStore jtiValidationStore; + @Inject + private CriterionOperatorRegistry criterionOperatorRegistry; @Provider(isDefault = true) public TrustedIssuerRegistry createInMemoryIssuerRegistry() { @@ -98,5 +103,9 @@ public ClaimTokenCreatorFunction defaultClaimTokenFunction() { return success(b.build()); }; } - + + @Provider(isDefault = true) + public DcpScopeStore scopeStore() { + return new InMemoryDcpScopeStore(criterionOperatorRegistry); + } } diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DynamicDcpScopeConfigurationExtension.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DynamicDcpScopeConfigurationExtension.java new file mode 100644 index 0000000000..b2100bcd43 --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DynamicDcpScopeConfigurationExtension.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.Config; + +import static org.eclipse.edc.iam.decentralizedclaims.core.DynamicDcpScopeConfigurationExtension.NAME; + + +@Extension(NAME) +public class DynamicDcpScopeConfigurationExtension implements ServiceExtension { + + public static final String NAME = "DCP Dynamic Scope Configuration Extension"; + + public static final String CONFIG_PREFIX = "edc.iam.dcp.scopes"; + public static final String CONFIG_ALIAS = CONFIG_PREFIX + ".."; + + @Setting(context = CONFIG_ALIAS, description = "ID of the scope.") + public static final String ID_SUFFIX = "id"; + @Setting(context = CONFIG_ALIAS, description = "Additional properties of the issuer.") + public static final String TYPE_SUFFIX = "type"; + @Setting(context = CONFIG_ALIAS, description = "The value of the scope.") + public static final String VALUE_SUFFIX = "value"; + + @Setting(context = CONFIG_ALIAS, description = "Prefix mapping for the scope.") + public static final String PREFIX_MAPPING_SUFFIX = "prefix-mapping"; + @Setting(context = CONFIG_ALIAS, description = "Profile the scope.", defaultValue = "*") + public static final String PROFILE_SUFFIX = "profile"; + + @Inject + private DcpScopeRegistry scopeRegistry; + + @Inject + private Monitor monitor; + + @Override + public void initialize(ServiceExtensionContext context) { + var config = context.getConfig(CONFIG_PREFIX); + var configs = config.partition().toList(); + configs.forEach(this::addScope); + + } + + private void addScope(Config config) { + var id = config.getString(ID_SUFFIX); + var type = config.getString(TYPE_SUFFIX); + var value = config.getString(VALUE_SUFFIX); + var prefixMapping = config.getString(PREFIX_MAPPING_SUFFIX, null); + var profile = config.getString(PROFILE_SUFFIX, "*"); + + var scope = DcpScope.Builder.newInstance().id(id) + .type(DcpScope.Type.valueOf(type.toUpperCase())) + .value(value) + .prefixMapping(prefixMapping) + .profile(profile) + .build(); + + scopeRegistry.register(scope).orElseThrow(e -> new EdcException("Failed to register DCP scope with id " + id)); + } + +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DynamicDcpScopeExtension.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DynamicDcpScopeExtension.java new file mode 100644 index 0000000000..ddd6dcaf32 --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/DynamicDcpScopeExtension.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core; + +import org.eclipse.edc.iam.decentralizedclaims.core.scope.DcpScopeRegistryImpl; +import org.eclipse.edc.iam.decentralizedclaims.core.scope.DefaultScopeMappingFunction; +import org.eclipse.edc.iam.decentralizedclaims.core.scope.DynamicScopeExtractor; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.ScopeExtractorRegistry; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.store.DcpScopeStore; +import org.eclipse.edc.policy.context.request.spi.RequestCatalogPolicyContext; +import org.eclipse.edc.policy.context.request.spi.RequestContractNegotiationPolicyContext; +import org.eclipse.edc.policy.context.request.spi.RequestTransferProcessPolicyContext; +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; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import static org.eclipse.edc.iam.decentralizedclaims.core.DynamicDcpScopeExtension.NAME; + +@Extension(NAME) +public class DynamicDcpScopeExtension implements ServiceExtension { + + public static final String NAME = "DCP Dynamic Scope Extension"; + + @Inject + private PolicyEngine policyEngine; + + @Inject + private TransactionContext transactionContext; + + @Inject + private DcpScopeStore scopeStore; + + + @Inject + private ScopeExtractorRegistry scopeExtractorRegistry; + + private DcpScopeRegistry registry; + + @Override + public void initialize(ServiceExtensionContext context) { + var contextMappingFunction = new DefaultScopeMappingFunction(registry()); + + policyEngine.registerPostValidator(RequestCatalogPolicyContext.class, contextMappingFunction::apply); + policyEngine.registerPostValidator(RequestContractNegotiationPolicyContext.class, contextMappingFunction::apply); + policyEngine.registerPostValidator(RequestTransferProcessPolicyContext.class, contextMappingFunction::apply); + + scopeExtractorRegistry.registerScopeExtractor(new DynamicScopeExtractor(registry())); + + } + + @Provider + public DcpScopeRegistry registry() { + if (registry == null) { + registry = new DcpScopeRegistryImpl(transactionContext, scopeStore); + } + return registry; + } +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DcpScopeRegistryImpl.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DcpScopeRegistryImpl.java new file mode 100644 index 0000000000..6fa39a162c --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DcpScopeRegistryImpl.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.store.DcpScopeStore; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.transaction.spi.TransactionContext; + +import java.util.List; + +/** + * Implementation of {@link DcpScopeRegistry}. + */ +public class DcpScopeRegistryImpl implements DcpScopeRegistry { + + private final TransactionContext transactionContext; + private final DcpScopeStore store; + + public DcpScopeRegistryImpl(TransactionContext transactionContext, DcpScopeStore store) { + this.transactionContext = transactionContext; + this.store = store; + } + + @Override + public ServiceResult register(DcpScope scope) { + return transactionContext.execute(() -> store.save(scope).flatMap(ServiceResult::from)); + } + + @Override + public ServiceResult remove(String scopeId) { + return transactionContext.execute(() -> store.delete(scopeId).flatMap(ServiceResult::from)); + } + + @Override + public ServiceResult> getDefaultScopes() { + var query = QuerySpec.Builder.newInstance() + .filter(Criterion.criterion("type", "=", DcpScope.Type.DEFAULT.name())) + .build(); + return transactionContext.execute(() -> store.query(query).flatMap(ServiceResult::from)); + } + + @Override + public ServiceResult> getScopeMapping() { + var query = QuerySpec.Builder.newInstance() + .filter(Criterion.criterion("type", "=", DcpScope.Type.POLICY.name())) + .build(); + return transactionContext.execute(() -> store.query(query).flatMap(ServiceResult::from)); + } +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DefaultScopeMappingFunction.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DefaultScopeMappingFunction.java new file mode 100644 index 0000000000..a42482c804 --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DefaultScopeMappingFunction.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; +import org.eclipse.edc.policy.engine.spi.PolicyValidatorRule; +import org.eclipse.edc.policy.model.Policy; + +import java.util.HashSet; + +import static org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope.WILDCARD; + +/** + * A policy validator rule that adds default DCP scopes to the request context based on the profile. + */ +public class DefaultScopeMappingFunction implements PolicyValidatorRule { + private final DcpScopeRegistry scopeRegistry; + + public DefaultScopeMappingFunction(DcpScopeRegistry scopeRegistry) { + this.scopeRegistry = scopeRegistry; + } + + @Override + public Boolean apply(Policy policy, RequestPolicyContext context) { + var defaultScopes = scopeRegistry.getDefaultScopes(); + if (defaultScopes.failed()) { + context.reportProblem("Failed to retrieve default scopes: " + defaultScopes.getFailureMessages()); + return false; + } + var defaultScopeList = defaultScopes.getContent().stream() + .filter(scope -> filterScope(scope, context)) + .map(DcpScope::getValue).toList(); + var requestScopeBuilder = context.requestScopeBuilder(); + var rq = requestScopeBuilder.build(); + var existingScope = rq.getScopes(); + var newScopes = new HashSet<>(defaultScopeList); + newScopes.addAll(existingScope); + requestScopeBuilder.scopes(newScopes); + return true; + } + + + boolean filterScope(DcpScope scope, RequestPolicyContext context) { + return scope.getProfile().equals(WILDCARD) || scope.getProfile().equals(context.requestContext().getMessage().getProtocol()); + } +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DynamicScopeExtractor.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DynamicScopeExtractor.java new file mode 100644 index 0000000000..a9ab07d920 --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DynamicScopeExtractor.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope; + +import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequestMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferRequestMessage; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.ScopeExtractor; +import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Extracts scopes dynamically from the DCP scope registry based on the request context. + */ +public class DynamicScopeExtractor implements ScopeExtractor { + private static final Set> SUPPORTED_MESSAGES = Set.of(CatalogRequestMessage.class, ContractRequestMessage.class, TransferRequestMessage.class); + private final DcpScopeRegistry registry; + + public DynamicScopeExtractor(DcpScopeRegistry registry) { + this.registry = registry; + } + + @Override + public Set extractScopes(Object leftValue, Operator operator, Object rightValue, RequestPolicyContext context) { + // extract only for supported messages + if (!SUPPORTED_MESSAGES.contains(context.requestContext().getMessage().getClass())) { + return Set.of(); + } + var result = registry.getScopeMapping(); + if (result.failed()) { + context.reportProblem("Failed to get scope mapping: " + result.getFailureMessages()); + return Set.of(); + } + return result.getContent().stream().filter(scope -> filterScope(scope, leftValue, context)) + .map(DcpScope::getValue) + .collect(Collectors.toSet()); + + } + + private boolean filterScope(DcpScope scope, Object leftValue, RequestPolicyContext context) { + if (leftValue instanceof String leftOperand) { + return (leftOperand.startsWith(scope.getPrefixMapping())) && + (scope.getProfile().equals(DcpScope.WILDCARD) || scope.getProfile().equals(context.requestContext().getMessage().getProtocol())); + } else { + return false; + } + } +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/defaults/InMemoryDcpScopeStore.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/defaults/InMemoryDcpScopeStore.java new file mode 100644 index 0000000000..8330a9cd27 --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/defaults/InMemoryDcpScopeStore.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope.defaults; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.store.DcpScopeStore; +import org.eclipse.edc.spi.query.CriterionOperatorRegistry; +import org.eclipse.edc.spi.query.QueryResolver; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.store.ReflectionBasedQueryResolver; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * In-memory implementation of {@link DcpScopeStore} for testing and prototyping purposes. + */ +public class InMemoryDcpScopeStore implements DcpScopeStore { + + private final QueryResolver queryResolver; + + private final Map scopes = new ConcurrentHashMap<>(); + + public InMemoryDcpScopeStore(CriterionOperatorRegistry criterionOperatorRegistry) { + this.queryResolver = new ReflectionBasedQueryResolver<>(DcpScope.class, criterionOperatorRegistry); + } + + @Override + public StoreResult save(DcpScope scope) { + scopes.put(scope.getId(), scope); + return StoreResult.success(); + } + + @Override + public StoreResult delete(String scopeId) { + scopes.remove(scopeId); + return StoreResult.success(); + } + + @Override + public StoreResult> query(QuerySpec spec) { + return StoreResult.success(queryResolver.query(scopes.values().stream(), spec) + .collect(Collectors.toList())); + } +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 9da32747e4..27ebe3ccb3 100644 --- a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -15,6 +15,8 @@ org.eclipse.edc.iam.decentralizedclaims.core.DcpDefaultServicesExtension org.eclipse.edc.iam.decentralizedclaims.core.DcpScopeExtractorExtension +org.eclipse.edc.iam.decentralizedclaims.core.DynamicDcpScopeExtension +org.eclipse.edc.iam.decentralizedclaims.core.DynamicDcpScopeConfigurationExtension org.eclipse.edc.iam.decentralizedclaims.core.DcpCoreExtension org.eclipse.edc.iam.decentralizedclaims.core.DcpTransformExtension org.eclipse.edc.iam.decentralizedclaims.core.DcpPresentationRequestExtension diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/DcpScopeConfigurationExtensionTest.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/DcpScopeConfigurationExtensionTest.java new file mode 100644 index 0000000000..2c6f1261c3 --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/DcpScopeConfigurationExtensionTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.ConfigFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +public class DcpScopeConfigurationExtensionTest { + + private final DcpScopeRegistry registry = mock(); + + @BeforeEach + void setup(ServiceExtensionContext context) { + context.registerService(DcpScopeRegistry.class, registry); + when(registry.register(any())).thenReturn(ServiceResult.success()); + } + + @Test + void initialize(ServiceExtensionContext context, DynamicDcpScopeConfigurationExtension ext) { + var cfg = ConfigFactory.fromMap(Map.of( + "membership.id", "membership-scope", + "membership.value", "org.eclipse.edc.vc.type:MembershipCredential:read", + "membership.type", "DEFAULT")); + when(context.getConfig("edc.iam.dcp.scopes")).thenReturn(cfg); + + ext.initialize(context); + + var captor = ArgumentCaptor.forClass(DcpScope.class); + verify(registry).register(captor.capture()); + + assertThat(captor.getValue().getId()).satisfies(scope -> { + assertThat(scope).isEqualTo("membership-scope"); + assertThat(captor.getValue().getValue()).isEqualTo("org.eclipse.edc.vc.type:MembershipCredential:read"); + assertThat(captor.getValue().getType()).isEqualTo(DcpScope.Type.DEFAULT); + }); + } + + @Test + void initialize_withPolicyType(ServiceExtensionContext context, DynamicDcpScopeConfigurationExtension ext) { + var cfg = ConfigFactory.fromMap(Map.of( + "membership.id", "membership-scope", + "membership.prefix-mapping", "Membership.", + "membership.value", "org.eclipse.edc.vc.type:MembershipCredential:read", + "membership.type", "POLICY")); + when(context.getConfig("edc.iam.dcp.scopes")).thenReturn(cfg); + + ext.initialize(context); + + var captor = ArgumentCaptor.forClass(DcpScope.class); + verify(registry).register(captor.capture()); + + assertThat(captor.getValue()).satisfies(scope -> { + assertThat(scope.getId()).isEqualTo("membership-scope"); + assertThat(scope.getValue()).isEqualTo("org.eclipse.edc.vc.type:MembershipCredential:read"); + assertThat(scope.getType()).isEqualTo(DcpScope.Type.POLICY); + assertThat(scope.getPrefixMapping()).isEqualTo("Membership."); + }); + } + +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DcpScopeRegistryImplTest.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DcpScopeRegistryImplTest.java new file mode 100644 index 0000000000..8ee2ac2a58 --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DcpScopeRegistryImplTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.store.DcpScopeStore; +import org.eclipse.edc.spi.result.StoreResult; +import org.eclipse.edc.transaction.spi.NoopTransactionContext; +import org.eclipse.edc.transaction.spi.TransactionContext; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class DcpScopeRegistryImplTest { + + + private final TransactionContext transactionContext = new NoopTransactionContext(); + private final DcpScopeStore store = mock(); + + @Test + void register() { + var scope = DcpScope.Builder.newInstance() + .id("s1") + .value("v") + .profile("p") + .build(); + + when(store.save(scope)).thenReturn(StoreResult.success()); + + var impl = new DcpScopeRegistryImpl(transactionContext, store); + var res = impl.register(scope); + + assertThat(res).isSucceeded(); + + verify(store).save(scope); + } + + @Test + void register_should_return_failure_when_store_fails() { + var scope = DcpScope.Builder.newInstance() + .id("s2") + .value("v") + .profile("p") + .build(); + + when(store.save(scope)).thenReturn(StoreResult.generalError("boom")); + + var impl = new DcpScopeRegistryImpl(transactionContext, store); + var res = impl.register(scope); + + assertThat(res).isFailed().detail().contains("boom"); + + verify(store).save(scope); + } + + @Test + void remove_should_delegate_to_store_and_return_success() { + when(store.delete("id")).thenReturn(StoreResult.success()); + + var impl = new DcpScopeRegistryImpl(transactionContext, store); + var res = impl.remove("id"); + + assertThat(res).isSucceeded(); + + verify(store).delete("id"); + } + + @Test + void getDefaultScopes_should_return_list_from_store() { + var s1 = DcpScope.Builder.newInstance().id("d1").value("v1").profile(DcpScope.WILDCARD).build(); + var s2 = DcpScope.Builder.newInstance().id("d2").value("v2").profile("p").build(); + var expected = List.of(s1, s2); + + when(store.query(any())).thenReturn(StoreResult.success(expected)); + + var impl = new DcpScopeRegistryImpl(transactionContext, store); + var res = impl.getDefaultScopes(); + + assertThat(res).isSucceeded().isEqualTo(expected); + verify(store).query(any()); + } + + @Test + void getScopeMapping_should_return_policy_scopes_from_store() { + var p1 = DcpScope.Builder.newInstance().id("p1").value("v1").type(DcpScope.Type.POLICY).profile("p").prefixMapping("pm").build(); + var expected = List.of(p1); + when(store.query(any())).thenReturn(StoreResult.success(expected)); + + var impl = new DcpScopeRegistryImpl(transactionContext, store); + var res = impl.getScopeMapping(); + + assertThat(res).isSucceeded().isEqualTo(expected); + verify(store).query(any()); + } +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DefaultScopeMappingFunctionTest.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DefaultScopeMappingFunctionTest.java new file mode 100644 index 0000000000..ce17e6cbda --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DefaultScopeMappingFunctionTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; +import org.eclipse.edc.policy.model.Policy; +import org.eclipse.edc.spi.iam.RequestContext; +import org.eclipse.edc.spi.iam.RequestScope; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +class DefaultScopeMappingFunctionTest { + + private final RequestPolicyContext context = mock(); + private final DcpScopeRegistry scopeRegistry = mock(); + + @Test + void apply() { + // prepare default scopes: one wildcard, one matching profile, one non-matching + var wildcardScope = DcpScope.Builder.newInstance() + .id("s1") + .value("v-wild") + .profile(DcpScope.WILDCARD) + .build(); + + var matchingScope = DcpScope.Builder.newInstance() + .id("s2") + .value("v-match") + .profile("proto") + .build(); + + var nonMatchingScope = DcpScope.Builder.newInstance() + .id("s3") + .value("v-non") + .profile("other") + .build(); + + + // existing scopes on request + var existing = new HashSet(); + existing.add("existing"); + var msg = mock(RemoteMessage.class); + when(msg.getProtocol()).thenReturn("proto"); + var ctx = RequestContext.Builder.newInstance() + .direction(RequestContext.Direction.Egress) + .message(msg) + .build(); + var builder = RequestScope.Builder.newInstance() + .scopes(existing); + + when(context.requestContext()).thenReturn(ctx); + when(context.requestScopeBuilder()).thenReturn(builder); + + // stub the chained calls using deep stubs + when(scopeRegistry.getDefaultScopes()).thenReturn(ServiceResult.success(List.of(wildcardScope, matchingScope, nonMatchingScope))); + + var fn = new DefaultScopeMappingFunction(scopeRegistry); + var result = fn.apply(mock(Policy.class), context); + assertTrue(result); + var setArg = builder.build().getScopes(); + assertThat(setArg).containsOnly("v-wild", "v-match", "existing"); + } + + @Test + void apply_shouldReport_whenServiceFails() { + when(scopeRegistry.getDefaultScopes()).thenReturn(ServiceResult.unexpected("Failed to retrieve default scopes")); + + var fn = new DefaultScopeMappingFunction(scopeRegistry); + + var result = fn.apply(mock(Policy.class), context); + + assertFalse(result); + verify(context).reportProblem(contains("Failed to retrieve default scopes")); + } +} \ No newline at end of file diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DynamicScopeExtractorTest.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DynamicScopeExtractorTest.java new file mode 100644 index 0000000000..e76b97125b --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/DynamicScopeExtractorTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope; + +import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage; +import org.eclipse.edc.connector.controlplane.contract.spi.types.negotiation.ContractRequestMessage; +import org.eclipse.edc.connector.controlplane.transfer.spi.types.protocol.TransferRequestMessage; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScopeRegistry; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.ScopeExtractor; +import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; +import org.eclipse.edc.policy.model.Operator; +import org.eclipse.edc.spi.iam.RequestContext; +import org.eclipse.edc.spi.result.ServiceResult; +import org.eclipse.edc.spi.types.domain.message.RemoteMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +class DynamicScopeExtractorTest { + + private final DcpScopeRegistry registry = mock(); + private final RequestPolicyContext context = mock(); + + @Test + void extractScope_shouldFail_whenRegistryFails() { + var msg = mock(TransferRequestMessage.class); + var ctx = RequestContext.Builder.newInstance() + .direction(RequestContext.Direction.Egress) + .message(msg) + .build(); + + when(context.requestContext()).thenReturn(ctx); + when(registry.getScopeMapping()).thenReturn(ServiceResult.unexpected("registry-error")); + + var extractor = new DynamicScopeExtractor(registry); + + Set scopes = extractor.extractScopes("any", Operator.EQ, null, context); + + assertTrue(scopes.isEmpty()); + verify(context).reportProblem(contains("Failed to get scope mapping")); + } + + @ParameterizedTest + @ArgumentsSource(MessageTypeProvider.class) + void extractScopes(RemoteMessage msg) { + // scopes: wildcard (should match), matching profile (should match), non-matching profile (excluded), wrong prefix (excluded) + var wildcard = DcpScope.Builder.newInstance() + .id("w") + .value("val-wild") + .type(DcpScope.Type.POLICY) + .prefixMapping("pre:") + .profile(DcpScope.WILDCARD) + .build(); + + var matching = DcpScope.Builder.newInstance() + .id("m") + .value("val-match") + .type(DcpScope.Type.POLICY) + .prefixMapping("pre:") + .profile("proto") + .build(); + + var nonMatchingProfile = DcpScope.Builder.newInstance() + .id("n1") + .value("val-non-profile") + .type(DcpScope.Type.POLICY) + .prefixMapping("pre:") + .profile("other") + .build(); + + var wrongPrefix = DcpScope.Builder.newInstance() + .id("n2") + .value("val-wrong-prefix") + .type(DcpScope.Type.POLICY) + .prefixMapping("other:") + .profile("proto") + .build(); + when(msg.getProtocol()).thenReturn("proto"); + + var ctx = RequestContext.Builder.newInstance() + .direction(RequestContext.Direction.Egress) + .message(msg) + .build(); + + when(registry.getScopeMapping()).thenReturn(ServiceResult.success(List.of(wildcard, matching, nonMatchingProfile, wrongPrefix))); + + when(context.requestContext()).thenReturn(ctx); + + var extractor = new DynamicScopeExtractor(registry); + + var result = extractor.extractScopes("pre:resource", Operator.EQ, null, context); + + assertThat(result).containsOnly("val-wild", "val-match"); + } + + @Test + void extractScopes_empty() { + var msg = mock(TransferRequestMessage.class); + var ctx = RequestContext.Builder.newInstance() + .direction(RequestContext.Direction.Egress) + .message(msg) + .build(); + + when(context.requestContext()).thenReturn(ctx); + when(registry.getScopeMapping()).thenReturn(ServiceResult.success(List.of())); + + ScopeExtractor extractor = new DynamicScopeExtractor(registry); + + Set result = extractor.extractScopes(12345, Operator.EQ, null, context); + assertTrue(result.isEmpty()); + } + + @Test + void extractScopes_empty_withWrongMessage() { + + var msg = mock(RemoteMessage.class); + var ctx = RequestContext.Builder.newInstance() + .direction(RequestContext.Direction.Egress) + .message(msg) + .build(); + + when(context.requestContext()).thenReturn(ctx); + + ScopeExtractor extractor = new DynamicScopeExtractor(registry); + + Set result = extractor.extractScopes(12345, Operator.EQ, null, context); + assertTrue(result.isEmpty()); + verifyNoInteractions(registry); + } + + public static class MessageTypeProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) { + return Stream.of( + arguments(mock(TransferRequestMessage.class)), + arguments(mock(CatalogRequestMessage.class)), + arguments(mock(ContractRequestMessage.class)) + ); + } + } +} diff --git a/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/defaults/InMemoryDcpScopeStoreTest.java b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/defaults/InMemoryDcpScopeStoreTest.java new file mode 100644 index 0000000000..7e40964d1d --- /dev/null +++ b/extensions/common/iam/decentralized-claims/decentralized-claims-core/src/test/java/org/eclipse/edc/iam/decentralizedclaims/core/scope/defaults/InMemoryDcpScopeStoreTest.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.core.scope.defaults; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.store.DcpScopeStore; +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.store.DcpScopeStoreTestBase; +import org.eclipse.edc.query.CriterionOperatorRegistryImpl; + + +public class InMemoryDcpScopeStoreTest extends DcpScopeStoreTestBase { + + private final InMemoryDcpScopeStore store = new InMemoryDcpScopeStore(CriterionOperatorRegistryImpl.ofDefaults()); + + @Override + protected DcpScopeStore getStore() { + return store; + } +} diff --git a/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/DcpScope.java b/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/DcpScope.java new file mode 100644 index 0000000000..7e70826012 --- /dev/null +++ b/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/DcpScope.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.spi.scope; + +import java.util.Objects; + +/** + * Represents a scope used by control plane. + * + *

A {@code DcpScope} encapsulates an identifier, a typed scope classification + * (see {@link Type}), a value that identifies the actual scope, + * an optional prefix mapping (required for {@link Type#POLICY}), + * and a profile string. By default, the {@code profile} is set to {@link #WILDCARD} + * and the {@code type} defaults to {@link Type#DEFAULT}.

+ */ +public class DcpScope { + + public static final String WILDCARD = "*"; + + public String profile = WILDCARD; + + private String id; + + private Type type = Type.DEFAULT; + + private String value; + + private String prefixMapping; + + private DcpScope() { + } + + public String getId() { + return id; + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + public String getProfile() { + return profile; + } + + public String getPrefixMapping() { + return prefixMapping; + } + + public enum Type { + DEFAULT, + POLICY, + } + + public static class Builder { + private final DcpScope scope; + + private Builder(DcpScope scope) { + this.scope = scope; + } + + public static Builder newInstance() { + return new Builder(new DcpScope()); + } + + public Builder id(String id) { + this.scope.id = id; + return this; + } + + public Builder type(Type type) { + this.scope.type = type; + return this; + } + + public Builder value(String value) { + this.scope.value = value; + return this; + } + + public Builder prefixMapping(String prefixMapping) { + this.scope.prefixMapping = prefixMapping; + return this; + } + + public Builder profile(String profile) { + this.scope.profile = profile; + return this; + } + + public DcpScope build() { + Objects.requireNonNull(scope.id, "DcpScope id cannot be null"); + Objects.requireNonNull(scope.value, "DcpScope value cannot be null"); + Objects.requireNonNull(scope.profile, "DcpScope profile cannot be null"); + + if (scope.type.equals(Type.POLICY)) { + Objects.requireNonNull(scope.prefixMapping, "DcpScope prefixMapping cannot be null for POLICY type"); + } + + if (scope.type.equals(Type.DEFAULT)) { + if (scope.prefixMapping != null) { + throw new IllegalArgumentException("Prefix mapping should be null for DEFAULT type"); + } + } + + return scope; + } + } +} diff --git a/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/DcpScopeRegistry.java b/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/DcpScopeRegistry.java new file mode 100644 index 0000000000..78bc582849 --- /dev/null +++ b/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/DcpScopeRegistry.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.spi.scope; + +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.result.ServiceResult; + +import java.util.List; + +/** + * Registry for DCP scopes. + */ +@ExtensionPoint +public interface DcpScopeRegistry { + + /** + * Registers a DCP scope. + * + * @param scope the scope to register + * @return a service result indicating success or failure + */ + ServiceResult register(DcpScope scope); + + /** + * Removes a DCP scope by its ID. + * + * @param scopeId the ID of the scope to remove + * @return a service result indicating success or failure + */ + ServiceResult remove(String scopeId); + + /** + * Retrieves the default DCP scopes. + * + * @return a service result containing the list of default scopes + */ + ServiceResult> getDefaultScopes(); + + /** + * Retrieves all scope mappings. + * + * @return a service result containing the list of all scope mappings + */ + ServiceResult> getScopeMapping(); + +} diff --git a/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/store/DcpScopeStore.java b/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/store/DcpScopeStore.java new file mode 100644 index 0000000000..9c853bd939 --- /dev/null +++ b/spi/common/decentralized-claims-spi/src/main/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/store/DcpScopeStore.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.spi.scope.store; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint; +import org.eclipse.edc.spi.query.QuerySpec; +import org.eclipse.edc.spi.result.StoreResult; + +import java.util.List; + +/** + * Store for DCP scopes. + */ +@ExtensionPoint +public interface DcpScopeStore { + + /** + * Saves a DCP scope. + * + * @param scope the scope to save + * @return a store result indicating success or failure + */ + StoreResult save(DcpScope scope); + + /** + * Deletes a DCP scope by its ID. + * + * @param scopeId the ID of the scope to delete + * @return a store result indicating success or failure + */ + StoreResult delete(String scopeId); + + /** + * Queries DCP scopes based on the provided query specification. + * + * @param spec the query specification + * @return a store result containing the list of matching scopes + */ + StoreResult> query(QuerySpec spec); +} diff --git a/spi/common/decentralized-claims-spi/src/testFixtures/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/store/DcpScopeStoreTestBase.java b/spi/common/decentralized-claims-spi/src/testFixtures/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/store/DcpScopeStoreTestBase.java new file mode 100644 index 0000000000..5fa802ec8b --- /dev/null +++ b/spi/common/decentralized-claims-spi/src/testFixtures/java/org/eclipse/edc/iam/decentralizedclaims/spi/scope/store/DcpScopeStoreTestBase.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2025 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.iam.decentralizedclaims.spi.scope.store; + +import org.eclipse.edc.iam.decentralizedclaims.spi.scope.DcpScope; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public abstract class DcpScopeStoreTestBase { + + /** + * Create a fresh store instance for each test. + *

+ * Implementations must ensure that the returned store is isolated (clean state). + */ + protected abstract DcpScopeStore getStore(); + + @Test + void save() { + var store = getStore(); + + var scope = DcpScope.Builder.newInstance() + .id("scope-1") + .value("value-1") + .profile("profile-a") + .build(); + + var saveResult = store.save(scope); + assertTrue(saveResult.succeeded(), "save should succeed"); + + var queryResult = store.query(QuerySpec.Builder.newInstance().build()); + assertTrue(queryResult.succeeded(), "query should succeed"); + var scopes = Objects.requireNonNull(queryResult.getContent()); + assertTrue(scopes.stream().anyMatch(s -> "scope-1".equals(s.getId())), "saved scope must be returned by query"); + } + + @Test + void delete() { + var store = getStore(); + + var scope = DcpScope.Builder.newInstance() + .id("scope-to-delete") + .value("v") + .profile("p") + .build(); + + var save = store.save(scope); + assertTrue(save.succeeded(), "save should succeed before delete"); + + var delete = store.delete(scope.getId()); + assertTrue(delete.succeeded(), "delete should succeed"); + + var queryResult = store.query(QuerySpec.Builder.newInstance().build()); + assertTrue(queryResult.succeeded(), "query should succeed after delete"); + var scopes = Objects.requireNonNull(queryResult.getContent()); + assertFalse(scopes.stream().anyMatch(s -> scope.getId().equals(s.getId())), "deleted scope must not be returned by query"); + } + + @Test + void query_withDefaultType() { + var store = getStore(); + + var scope = DcpScope.Builder.newInstance() + .id("scope-1") + .value("value-1") + .profile("profile-a") + .build(); + + var saveResult = store.save(scope); + assertTrue(saveResult.succeeded(), "save should succeed"); + + var queryResult = store.query(QuerySpec.Builder.newInstance() + .filter(Criterion.criterion("type", "=", "DEFAULT")) + .build()); + assertTrue(queryResult.succeeded(), "query should succeed"); + var scopes = Objects.requireNonNull(queryResult.getContent()); + assertTrue(scopes.stream().anyMatch(s -> "scope-1".equals(s.getId())), "saved scope must be returned by query"); + } + + @Test + void query_withPolicyType() { + var store = getStore(); + + var scope = DcpScope.Builder.newInstance() + .id("scope-1") + .value("value-1") + .profile("profile-a") + .type(DcpScope.Type.POLICY) + .prefixMapping("prefix-mapping-1") + .build(); + + var saveResult = store.save(scope); + assertTrue(saveResult.succeeded(), "save should succeed"); + + var queryResult = store.query(QuerySpec.Builder.newInstance() + .filter(Criterion.criterion("type", "=", "POLICY")) + .build()); + assertTrue(queryResult.succeeded(), "query should succeed"); + var scopes = Objects.requireNonNull(queryResult.getContent()); + assertTrue(scopes.stream().anyMatch(s -> "scope-1".equals(s.getId())), "saved scope must be returned by query"); + } + +} \ No newline at end of file diff --git a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowTest.java b/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowTest.java index 1442266107..ba96bc0b7d 100644 --- a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowTest.java +++ b/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowTest.java @@ -37,7 +37,6 @@ import org.eclipse.edc.junit.extensions.EmbeddedRuntime; import org.eclipse.edc.junit.extensions.RuntimePerClassExtension; import org.eclipse.edc.spi.iam.TokenRepresentation; -import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.configuration.ConfigFactory; import org.eclipse.edc.test.e2e.tck.TckTest; import org.junit.jupiter.api.Assertions; @@ -87,7 +86,6 @@ public class DcpPresentationFlowTest { static final RuntimePerClassExtension EDC_RUNTIME_EXTENSIONS = new RuntimePerClassExtension( new EmbeddedRuntime("Connector-under-test", ":dist:bom:controlplane-dcp-bom") .registerServiceMock(SecureTokenService.class, STS_MOCK) - .registerSystemExtension(ServiceExtension.class, new DefaultScopeFunctionExtension()) .configurationProvider(() -> ConfigFactory.fromMap(Map.of( "edc.iam.accesstoken.jti.validation", "true", "edc.iam.did.web.use.https", "false", @@ -100,6 +98,11 @@ public class DcpPresentationFlowTest { "edc.iam.sts.oauth.client.id", "test-client-id", "edc.iam.sts.oauth.client.secret.alias", "test-secret-alias" ))) + .configurationProvider(() -> ConfigFactory.fromMap(Map.of( + "edc.iam.dcp.scopes.membership.id", "membership-scope", + "edc.iam.dcp.scopes.membership.type", "DEFAULT", + "edc.iam.dcp.scopes.membership.value", "org.eclipse.dspace.dcp.vc.type:MembershipCredential:read" + ))) ); @RegisterExtension diff --git a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowWithDockerTest.java b/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowWithDockerTest.java index 1e5658cb96..31ce2f1640 100644 --- a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowWithDockerTest.java +++ b/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DcpPresentationFlowWithDockerTest.java @@ -37,7 +37,6 @@ import org.eclipse.edc.junit.extensions.RuntimePerClassExtension; import org.eclipse.edc.spi.iam.TokenRepresentation; import org.eclipse.edc.spi.monitor.ConsoleMonitor; -import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.configuration.ConfigFactory; import org.eclipse.edc.test.e2e.tck.TckTest; import org.junit.jupiter.api.BeforeEach; @@ -90,7 +89,6 @@ public class DcpPresentationFlowWithDockerTest { static final RuntimePerClassExtension EDC_RUNTIME_EXTENSIONS = new RuntimePerClassExtension( new EmbeddedRuntime("Connector-under-test", ":dist:bom:controlplane-dcp-bom") .registerServiceMock(SecureTokenService.class, STS_MOCK) - .registerSystemExtension(ServiceExtension.class, new DefaultScopeFunctionExtension()) .configurationProvider(() -> ConfigFactory.fromMap(Map.of( "edc.iam.accesstoken.jti.validation", "true", "edc.iam.did.web.use.https", "false", @@ -102,6 +100,11 @@ public class DcpPresentationFlowWithDockerTest { "edc.iam.sts.oauth.client.id", "test-client-id", "edc.iam.sts.oauth.client.secret.alias", "test-secret-alias" ))) + .configurationProvider(() -> ConfigFactory.fromMap(Map.of( + "edc.iam.dcp.scopes.membership.id", "membership-scope", + "edc.iam.dcp.scopes.membership.type", "DEFAULT", + "edc.iam.dcp.scopes.membership.value", "org.eclipse.dspace.dcp.vc.type:MembershipCredential:read" + ))) ); @RegisterExtension diff --git a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DefaultScopeFunctionExtension.java b/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DefaultScopeFunctionExtension.java deleted file mode 100644 index 6a69905055..0000000000 --- a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DefaultScopeFunctionExtension.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2024 Metaform Systems, Inc. - * - * 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: - * Metaform Systems, Inc. - initial API and implementation - * - */ - -package org.eclipse.edc.test.e2e.tck.presentation; - -import org.eclipse.edc.policy.context.request.spi.RequestCatalogPolicyContext; -import org.eclipse.edc.policy.context.request.spi.RequestContractNegotiationPolicyContext; -import org.eclipse.edc.policy.context.request.spi.RequestTransferProcessPolicyContext; -import org.eclipse.edc.policy.engine.spi.PolicyEngine; -import org.eclipse.edc.runtime.metamodel.annotation.Inject; -import org.eclipse.edc.spi.system.ServiceExtension; -import org.eclipse.edc.spi.system.ServiceExtensionContext; - -import java.util.Set; - -/** - * This extension registers a default scope mapping function, which causes a particular scope to be added to every - * Presentation Query - */ -public class DefaultScopeFunctionExtension implements ServiceExtension { - - @Inject - private PolicyEngine policyEngine; - - @Override - public void initialize(ServiceExtensionContext context) { - - // register a default scope provider - var contextMappingFunction = new DefaultScopeMappingFunction(Set.of("org.eclipse.dspace.dcp.vc.type:MembershipCredential:read")); - - policyEngine.registerPostValidator(RequestCatalogPolicyContext.class, contextMappingFunction::apply); - policyEngine.registerPostValidator(RequestContractNegotiationPolicyContext.class, contextMappingFunction::apply); - policyEngine.registerPostValidator(RequestTransferProcessPolicyContext.class, contextMappingFunction::apply); - - } -} diff --git a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DefaultScopeMappingFunction.java b/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DefaultScopeMappingFunction.java deleted file mode 100644 index 40bfb41d0a..0000000000 --- a/system-tests/dcp-tck-tests/presentation/src/test/java/org/eclipse/edc/test/e2e/tck/presentation/DefaultScopeMappingFunction.java +++ /dev/null @@ -1,43 +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.test.e2e.tck.presentation; - -import org.eclipse.edc.policy.context.request.spi.RequestPolicyContext; -import org.eclipse.edc.policy.engine.spi.PolicyValidatorRule; -import org.eclipse.edc.policy.model.Policy; - -import java.util.HashSet; -import java.util.Set; - -/** - * This function adds a set of preconfigured scopes to the outbound DCP request - * - * @param defaultScopes a set of scope strings that are attached to every DCP PresentationQuery - * - */ -public record DefaultScopeMappingFunction( - Set defaultScopes) implements PolicyValidatorRule { - - @Override - public Boolean apply(Policy policy, RequestPolicyContext requestPolicyContext) { - var requestScopeBuilder = requestPolicyContext.requestScopeBuilder(); - var rq = requestScopeBuilder.build(); - var existingScope = rq.getScopes(); - var newScopes = new HashSet<>(defaultScopes); - newScopes.addAll(existingScope); - requestScopeBuilder.scopes(newScopes); - return true; - } -}