Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add additional scopes to the CatalogRequest #4348

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,9 @@ maven/mavencentral/io.cloudevents/cloudevents-core/4.0.1, Apache-2.0, approved,
maven/mavencentral/io.cloudevents/cloudevents-http-basic/4.0.1, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.github.classgraph/classgraph/4.8.154, MIT, approved, CQ22530
maven/mavencentral/io.github.classgraph/classgraph/4.8.165, MIT, approved, CQ22530
maven/mavencentral/io.micrometer/micrometer-commons/1.13.1, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14826
maven/mavencentral/io.micrometer/micrometer-core/1.13.1, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14827
maven/mavencentral/io.micrometer/micrometer-observation/1.13.1, Apache-2.0, approved, #14829
maven/mavencentral/io.micrometer/micrometer-commons/1.13.2, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14826
maven/mavencentral/io.micrometer/micrometer-core/1.13.2, Apache-2.0 AND (Apache-2.0 AND MIT), approved, #14827
maven/mavencentral/io.micrometer/micrometer-observation/1.13.2, Apache-2.0, approved, #14829
maven/mavencentral/io.netty/netty-buffer/4.1.86.Final, Apache-2.0, approved, CQ21842
maven/mavencentral/io.netty/netty-codec-http/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
maven/mavencentral/io.netty/netty-codec-http2/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
Expand All @@ -136,7 +136,7 @@ maven/mavencentral/io.prometheus/simpleclient_tracer_otel/0.16.0, Apache-2.0, ap
maven/mavencentral/io.prometheus/simpleclient_tracer_otel_agent/0.16.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.rest-assured/json-path/5.5.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.rest-assured/rest-assured-common/5.5.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.rest-assured/rest-assured/5.5.0, Apache-2.0, restricted, clearlydefined
maven/mavencentral/io.rest-assured/rest-assured/5.5.0, Apache-2.0, approved, #15676
maven/mavencentral/io.rest-assured/xml-path/5.5.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.setl/rdf-urdna/1.1, Apache-2.0, approved, clearlydefined
maven/mavencentral/io.swagger.core.v3/swagger-annotations-jakarta/2.2.22, Apache-2.0, approved, #5947
Expand Down Expand Up @@ -188,6 +188,7 @@ maven/mavencentral/net.bytebuddy/byte-buddy-agent/1.14.15, Apache-2.0, approved,
maven/mavencentral/net.bytebuddy/byte-buddy/1.14.1, Apache-2.0 AND BSD-3-Clause, approved, #7163
maven/mavencentral/net.bytebuddy/byte-buddy/1.14.15, Apache-2.0 AND BSD-3-Clause, approved, #7163
maven/mavencentral/net.bytebuddy/byte-buddy/1.14.16, Apache-2.0 AND BSD-3-Clause, approved, #7163
maven/mavencentral/net.bytebuddy/byte-buddy/1.14.18, Apache-2.0 AND BSD-3-Clause, approved, #7163
maven/mavencentral/net.java.dev.jna/jna/5.13.0, Apache-2.0 AND LGPL-2.1-or-later, approved, #15196
maven/mavencentral/net.javacrumbs.json-unit/json-unit-core/2.36.0, Apache-2.0, approved, clearlydefined
maven/mavencentral/net.minidev/accessors-smart/2.4.7, Apache-2.0, approved, #7515
Expand Down Expand Up @@ -228,6 +229,7 @@ maven/mavencentral/org.apache.velocity/velocity-engine-scripting/2.3, Apache-2.0
maven/mavencentral/org.apache.xbean/xbean-reflect/3.7, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.apiguardian/apiguardian-api/1.1.2, Apache-2.0, approved, clearlydefined
maven/mavencentral/org.assertj/assertj-core/3.26.0, Apache-2.0, approved, #14886
maven/mavencentral/org.assertj/assertj-core/3.26.3, Apache-2.0, approved, #14886
maven/mavencentral/org.awaitility/awaitility/4.2.1, Apache-2.0, approved, #14178
maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.72, MIT, approved, #3789
maven/mavencentral/org.bouncycastle/bcpkix-jdk18on/1.78.1, MIT, approved, #14434
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ public CatalogServiceImpl(RemoteMessageDispatcherRegistry dispatcher) {
}

@Override
public CompletableFuture<StatusResult<byte[]>> requestCatalog(String counterPartyId, String counterPartyAddress, String protocol, QuerySpec querySpec) {
public CompletableFuture<StatusResult<byte[]>> requestCatalog(String counterPartyId, String counterPartyAddress, String protocol, QuerySpec querySpec, String... additionalScopes) {
var request = CatalogRequestMessage.Builder.newInstance()
.protocol(protocol)
.counterPartyId(counterPartyId)
.counterPartyAddress(counterPartyAddress)
.querySpec(querySpec)
.additionalScopes(additionalScopes)
.build();

return dispatcher.dispatch(byte[].class, request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import okhttp3.Response;
import okhttp3.ResponseBody;
import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage;
import org.eclipse.edc.http.spi.EdcHttpClient;
import org.eclipse.edc.policy.engine.spi.PolicyContextImpl;
import org.eclipse.edc.policy.engine.spi.PolicyEngine;
Expand Down Expand Up @@ -108,6 +109,11 @@ public <T, M extends RemoteMessage> CompletableFuture<StatusResult<T>> dispatch(
var policyProvider = (Function<M, Policy>) policyScope.policyProvider;
policyEngine.evaluate(policyScope.scope, policyProvider.apply(message), context);

// catalog request messages can carry additional, user-supplied scopes
if (message instanceof CatalogRequestMessage catalogRequestMessage) {
catalogRequestMessage.getAdditionalScopes().forEach(requestScopeBuilder::scope);
}

var scopes = requestScopeBuilder.build().getScopes();

// Only add the scope claim if there are scopes returned from the policy engine evaluation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.ResponseBody;
import org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequestMessage;
import org.eclipse.edc.http.spi.EdcHttpClient;
import org.eclipse.edc.policy.engine.spi.PolicyContext;
import org.eclipse.edc.policy.engine.spi.PolicyEngine;
Expand Down Expand Up @@ -271,6 +272,50 @@ void dispatch_shouldNotEvaluatePolicy_whenItIsNotRegistered() {
verifyNoInteractions(policyEngine);
}

@Test
void dispatch_whenCatalogRequestMessage_shouldExtractScopes() {
var authToken = "token";
Map<String, Object> additional = Map.of("foo", "bar");
var policy = Policy.Builder.newInstance().build();
DspHttpRequestFactory<CatalogRequestMessage> rqFactory = mock();
when(audienceResolver.resolve(any())).thenReturn(Result.success(AUDIENCE_VALUE));
when(tokenDecorator.decorate(any())).thenAnswer(a -> a.getArgument(0, TokenParameters.Builder.class).claims(additional));
when(rqFactory.createRequest(any())).thenReturn(new Request.Builder().url("http://url").build());
when(httpClient.executeAsync(any(), isA(List.class))).thenReturn(completedFuture(dummyResponse(200)));
when(identityService.obtainClientCredentials(any()))
.thenReturn(Result.success(TokenRepresentation.Builder.newInstance().token(authToken).build()));

dispatcher.registerPolicyScope(CatalogRequestMessage.class, "scope.test", (m) -> policy);

when(policyEngine.evaluate(eq("scope.test"), eq(policy), any())).thenAnswer(a -> {
PolicyContext context = a.getArgument(2);
var builder = context.getContextData(RequestScope.Builder.class);
builder.scope("policy-test-scope");
return Result.success();
});

dispatcher.registerMessage(CatalogRequestMessage.class, rqFactory, mock());

var message = CatalogRequestMessage.Builder.newInstance().additionalScopes("scope1", "scope2").build();
var result = dispatcher.dispatch(String.class, message);

assertThat(result).succeedsWithin(timeout);

var captor = ArgumentCaptor.forClass(TokenParameters.class);
verify(identityService).obtainClientCredentials(captor.capture());
verify(httpClient).executeAsync(argThat(r -> authToken.equals(r.headers().get("Authorization"))), isA(List.class));
verify(rqFactory).createRequest(message);
verify(policyEngine).evaluate(any(), any(), argThat(ctx -> {
var requestContext = ctx.getContextData(RequestContext.class);
return requestContext.getMessage().getClass().equals(CatalogRequestMessage.class) && requestContext.getDirection().equals(RequestContext.Direction.Egress);
}));
assertThat(captor.getValue()).satisfies(tr -> {
assertThat(tr.getStringClaim(SCOPE_CLAIM)).isEqualTo("policy-test-scope scope1 scope2");
assertThat(tr.getStringClaim(AUDIENCE_CLAIM)).isEqualTo(AUDIENCE_VALUE);
assertThat(tr.getClaims()).containsAllEntriesOf(additional);
});
}

@Test
void dispatch_shouldEvaluatePolicy() {
var policy = Policy.Builder.newInstance().build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
{
"version": "3.1.0-alpha",
"urlPath": "/v3.1alpha",
"lastUpdated": "2024-07-09T09:17:00Z",
"lastUpdated": "2024-07-10T09:17:00Z",
"maturity": "alpha"
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public void requestCatalog(JsonObject requestBody, AsyncResponse response) {
var request = transformerRegistry.transform(requestBody, CatalogRequest.class)
.orElseThrow(InvalidRequestException::new);

service.requestCatalog(request.getCounterPartyId(), request.getCounterPartyAddress(), request.getProtocol(), request.getQuerySpec())
var scopes = request.getAdditionalScopes().toArray(new String[0]);
service.requestCatalog(request.getCounterPartyId(), request.getCounterPartyAddress(), request.getProtocol(), request.getQuerySpec(), scopes)
.whenComplete((result, throwable) -> {
try {
response.resume(toResponse(result, throwable));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.edc.connector.controlplane.api.management.catalog.transform.JsonObjectToDatasetRequestTransformer;
import org.eclipse.edc.connector.controlplane.api.management.catalog.v2.CatalogApiV2Controller;
import org.eclipse.edc.connector.controlplane.api.management.catalog.v3.CatalogApiV3Controller;
import org.eclipse.edc.connector.controlplane.api.management.catalog.v31alpha.CatalogApiV31AlphaController;
import org.eclipse.edc.connector.controlplane.api.management.catalog.validation.CatalogRequestValidator;
import org.eclipse.edc.connector.controlplane.api.management.catalog.validation.DatasetRequestValidator;
import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogService;
Expand Down Expand Up @@ -67,6 +68,7 @@ public void initialize(ServiceExtensionContext context) {
var managementApiTransformerRegistry = transformerRegistry.forContext("management-api");
webService.registerResource(ApiContext.MANAGEMENT, new CatalogApiV2Controller(service, managementApiTransformerRegistry, validatorRegistry, context.getMonitor()));
webService.registerResource(ApiContext.MANAGEMENT, new CatalogApiV3Controller(service, managementApiTransformerRegistry, validatorRegistry));
webService.registerResource(ApiContext.MANAGEMENT, new CatalogApiV31AlphaController(service, managementApiTransformerRegistry, validatorRegistry));

validatorRegistry.register(CATALOG_REQUEST_TYPE, CatalogRequestValidator.instance(criterionOperatorRegistry));
validatorRegistry.register(DATASET_REQUEST_TYPE, DatasetRequestValidator.instance());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Optional;

import static java.util.Optional.ofNullable;
import static org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequest.CATALOG_REQUEST_ADDITIONAL_SCOPES;
import static org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequest.CATALOG_REQUEST_COUNTER_PARTY_ADDRESS;
import static org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequest.CATALOG_REQUEST_COUNTER_PARTY_ID;
import static org.eclipse.edc.connector.controlplane.catalog.spi.CatalogRequest.CATALOG_REQUEST_PROTOCOL;
Expand All @@ -40,21 +40,25 @@ public JsonObjectToCatalogRequestTransformer() {
var counterPartyAddress = transformString(object.get(CATALOG_REQUEST_COUNTER_PARTY_ADDRESS), context);

// For backward compatibility if the ID is not sent, fallback to the counterPartyAddress
var counterPartyId = Optional.ofNullable(object.get(CATALOG_REQUEST_COUNTER_PARTY_ID))
var counterPartyId = ofNullable(object.get(CATALOG_REQUEST_COUNTER_PARTY_ID))
.map(it -> transformString(it, context))
.orElse(counterPartyAddress);

var querySpec = Optional.of(object)
.map(it -> it.get(CATALOG_REQUEST_QUERY_SPEC))
var querySpec = ofNullable(object.get(CATALOG_REQUEST_QUERY_SPEC))
.map(it -> transformObject(it, QuerySpec.class, context))
.orElse(null);

return CatalogRequest.Builder.newInstance()
var builder = CatalogRequest.Builder.newInstance()
.protocol(transformString(object.get(CATALOG_REQUEST_PROTOCOL), context))
.counterPartyAddress(counterPartyAddress)
.counterPartyId(counterPartyId)
.querySpec(querySpec)
.build();
.querySpec(querySpec);


ofNullable(object.getJsonArray(CATALOG_REQUEST_ADDITIONAL_SCOPES))
.ifPresent(ja -> builder.additionalScopes(ja.stream().map(this::nodeValue).toList()));

return builder.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0
*
* SPDX-License-Identifier: Apache-2.0
*
* Contributors:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.connector.controlplane.api.management.catalog.v31alpha;

import jakarta.json.JsonObject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.AsyncResponse;
import jakarta.ws.rs.container.Suspended;
import org.eclipse.edc.connector.controlplane.api.management.catalog.BaseCatalogApiController;
import org.eclipse.edc.connector.controlplane.services.spi.catalog.CatalogService;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;

import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;

@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Path("/v3.1alpha/catalog")
public class CatalogApiV31AlphaController extends BaseCatalogApiController implements CatalogApiV31alpha {
public CatalogApiV31AlphaController(CatalogService service, TypeTransformerRegistry transformerRegistry, JsonObjectValidatorRegistry validatorRegistry) {
super(service, transformerRegistry, validatorRegistry);
}

@POST
@Path("/request")
@Override
public void requestCatalogV31alpha(JsonObject request, @Suspended AsyncResponse response) {
requestCatalog(request, response);
}

@POST
@Path("dataset/request")
@Override
public void getDatasetV31alpha(JsonObject request, @Suspended AsyncResponse response) {
getDataset(request, response);
}
}
Loading
Loading