Skip to content

Commit

Permalink
feat: enable Multi-Tenancy in PresentationApi (eclipse-edc#263)
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger authored Feb 6, 2024
1 parent f1dc1fd commit 2393fdf
Show file tree
Hide file tree
Showing 23 changed files with 459 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToPresentationQueryTransformer;
import org.eclipse.edc.identityhub.api.v1.PresentationApiController;
import org.eclipse.edc.identityhub.api.validation.PresentationQueryValidator;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
Expand Down Expand Up @@ -60,6 +61,8 @@ public class PresentationApiExtension implements ServiceExtension {
private JsonLd jsonLd;
@Inject
private TypeManager typeManager;
@Inject
private ParticipantContextService participantContextService;

@Override
public String name() {
Expand All @@ -72,7 +75,7 @@ public void initialize(ServiceExtensionContext context) {
validatorRegistry.register(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY, new PresentationQueryValidator());


var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, verifiablePresentationService, context.getMonitor());
var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, verifiablePresentationService, context.getMonitor(), participantContextService);

var jsonLdMapper = typeManager.getMapper(JSON_LD);
webService.registerResource(RESOLUTION_CONTEXT, new ObjectMapperProvider(jsonLdMapper));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ public interface PresentationApi {
@ApiResponse(responseCode = "401", description = "No Authorization header was given.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "403", description = "The given authentication token could not be validated. This can happen, when the request body " +
"calls for a broader query scope than the granted scope in the auth token",
"calls for a broader query scope than the granted scope in the auth token",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "501", description = "When the request contained a presentationDefinition object, but the implementation does not support it.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json"))
}
)
Response queryPresentation(JsonObject query, String authHeader);
Response queryPresentation(String participantContextId, JsonObject query, String authHeader);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage;
Expand All @@ -43,10 +46,11 @@
import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY;
import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper;

@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@Path("/presentation")
@Path("/participants/{participantId}/presentation")
public class PresentationApiController implements PresentationApi {

private final JsonObjectValidatorRegistry validatorRegistry;
Expand All @@ -55,22 +59,24 @@ public class PresentationApiController implements PresentationApi {
private final AccessTokenVerifier accessTokenVerifier;
private final VerifiablePresentationService verifiablePresentationService;
private final Monitor monitor;
private final ParticipantContextService participantContextService;

public PresentationApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry, CredentialQueryResolver queryResolver,
AccessTokenVerifier accessTokenVerifier, VerifiablePresentationService verifiablePresentationService, Monitor monitor) {
AccessTokenVerifier accessTokenVerifier, VerifiablePresentationService verifiablePresentationService, Monitor monitor, ParticipantContextService participantContextService) {
this.validatorRegistry = validatorRegistry;
this.transformerRegistry = transformerRegistry;
this.queryResolver = queryResolver;
this.accessTokenVerifier = accessTokenVerifier;
this.verifiablePresentationService = verifiablePresentationService;
this.monitor = monitor;
this.participantContextService = participantContextService;
}


@POST
@Path("/query")
@Override
public Response queryPresentation(JsonObject query, @HeaderParam(AUTHORIZATION) String token) {
public Response queryPresentation(@PathParam("participantId") String participantContextId, JsonObject query, @HeaderParam(AUTHORIZATION) String token) {
if (token == null) {
throw new AuthenticationFailedException("Authorization header missing");
}
Expand All @@ -83,15 +89,20 @@ public Response queryPresentation(JsonObject query, @HeaderParam(AUTHORIZATION)
return notImplemented();
}

// verify that the participant actually exists
participantContextService.getParticipantContext(participantContextId)
.orElseThrow(exceptionMapper(ParticipantContext.class, participantContextId));


// verify and validate the requestor's SI token
var issuerScopes = accessTokenVerifier.verify(token).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail())));
var issuerScopes = accessTokenVerifier.verify(token, participantContextId).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail())));

// query the database
var credentials = queryResolver.query(presentationQuery, issuerScopes).orElseThrow(f -> new NotAuthorizedException(f.getFailureDetail()));
var credentials = queryResolver.query(participantContextId, presentationQuery, issuerScopes).orElseThrow(f -> new NotAuthorizedException(f.getFailureDetail()));

// package the credentials in a VP and sign
var audience = getAudience(token);
var presentationResponse = verifiablePresentationService.createPresentation(credentials.toList(), presentationQuery.getPresentationDefinition(), audience)
var presentationResponse = verifiablePresentationService.createPresentation(participantContextId, credentials.toList(), presentationQuery.getPresentationDefinition(), audience)
.compose(presentation -> transformerRegistry.transform(presentation, JsonObject.class))
.orElseThrow(failure -> new EdcException("Error creating VerifiablePresentation: %s".formatted(failure.getFailureDetail())));
return Response.ok()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
import jakarta.json.Json;
import jakarta.json.JsonObject;
import org.eclipse.edc.identityhub.api.v1.PresentationApiController;
import org.eclipse.edc.identityhub.spi.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService;
import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.resolution.QueryResult;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
Expand All @@ -30,6 +32,7 @@
import org.eclipse.edc.junit.annotations.ApiTest;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.spi.result.ServiceResult;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase;
Expand Down Expand Up @@ -69,16 +72,20 @@
@SuppressWarnings("resource")
class PresentationApiControllerTest extends RestControllerTestBase {

private static final String PARTICIPANT_ID = "participant-id";
private final JsonObjectValidatorRegistry validatorRegistryMock = mock();
private final TypeTransformerRegistry typeTransformerRegistry = mock();
private final CredentialQueryResolver queryResolver = mock();
private final AccessTokenVerifier accessTokenVerifier = mock();
private final VerifiablePresentationService generator = mock();

private final ParticipantContextService participantContextService = mock(a -> ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId(a.getArgument(0).toString())
.apiTokenAlias("test-alias")
.build()));

@Test
void query_tokenNotPresent_shouldReturn401() {
assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), null))
assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), null))
.isInstanceOf(AuthenticationFailedException.class)
.hasMessage("Authorization header missing");
}
Expand All @@ -87,7 +94,7 @@ void query_tokenNotPresent_shouldReturn401() {
void query_validationError_shouldReturn400() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(failure(violation("foo", "bar")));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt()))
.isInstanceOf(ValidationFailureException.class)
.hasMessage("foo");
}
Expand All @@ -97,7 +104,7 @@ void query_transformationError_shouldReturn400() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.failure("cannot transform"));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt()))
.isInstanceOf(InvalidRequestException.class)
.hasMessage("cannot transform");
verifyNoInteractions(accessTokenVerifier, queryResolver, generator);
Expand All @@ -110,7 +117,7 @@ void query_withPresentationDefinition_shouldReturn501() {
.presentationDefinition(PresentationDefinition.Builder.newInstance().id(UUID.randomUUID().toString()).build());
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder.build()));

var response = controller().queryPresentation(createObjectBuilder().build(), generateJwt());
var response = controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt());
assertThat(response.getStatus()).isEqualTo(503);
assertThat(response.getEntity()).extracting(o -> (ApiErrorDetail) o).satisfies(ed -> {
assertThat(ed.getMessage()).isEqualTo("Not implemented.");
Expand All @@ -125,9 +132,9 @@ void query_tokenVerificationFails_shouldReturn401() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.failure("test-failure"));
when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.failure("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt()))
.isExactlyInstanceOf(AuthenticationFailedException.class)
.hasMessage("ID token verification failed: test-failure");
verifyNoInteractions(queryResolver, generator);
Expand All @@ -138,10 +145,10 @@ void query_queryResolutionFails_shouldReturn403() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(QueryResult.unauthorized("test-failure"));
when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(anyString(), any(), eq(List.of("test-scope1")))).thenReturn(QueryResult.unauthorized("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt()))
.isInstanceOf(NotAuthorizedException.class)
.hasMessage("test-failure");
verifyNoInteractions(generator);
Expand All @@ -152,12 +159,12 @@ void query_presentationGenerationFails_shouldReturn500() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));
when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(anyString(), any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));

when(generator.createPresentation(anyList(), any(), any())).thenReturn(Result.failure("test-failure"));
when(generator.createPresentation(anyString(), anyList(), any(), any())).thenReturn(Result.failure("test-failure"));

assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt()))
assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt()))
.isExactlyInstanceOf(EdcException.class)
.hasMessage("Error creating VerifiablePresentation: test-failure");
}
Expand All @@ -167,18 +174,18 @@ void query_success() {
when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success());
var presentationQueryBuilder = createPresentationQueryBuilder().build();
when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder));
when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));
when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("test-scope1")));
when(queryResolver.query(anyString(), any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty()));

var pres = PresentationResponseMessage.Builder.newinstance().presentation(List.of(generateJwt()))
.presentationSubmission(new PresentationSubmission("id", "def-id", List.of(new InputDescriptorMapping("id", "ldp_vp", "$.verifiableCredentials[0]"))))
.build();

var jsonResponse = Json.createObjectBuilder().build();
when(typeTransformerRegistry.transform(eq(pres), eq(JsonObject.class))).thenReturn(Result.success(jsonResponse));
when(generator.createPresentation(anyList(), any(), any())).thenReturn(Result.success(pres));
when(generator.createPresentation(anyString(), anyList(), any(), any())).thenReturn(Result.success(pres));

var response = controller().queryPresentation(createObjectBuilder().build(), generateJwt());
var response = controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt());
assertThat(response).isNotNull();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getEntity()).isEqualTo(jsonResponse);
Expand All @@ -187,11 +194,11 @@ void query_success() {

@Override
protected PresentationApiController controller() {
return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock());
return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock(), participantContextService);
}

private String generateJwt() {
var ecKey = generateEcKey();
var ecKey = generateEcKey(null);
var jwt = buildSignedJwt(new JWTClaimsSet.Builder().audience("test-audience")
.expirationTime(Date.from(Instant.now().plusSeconds(3600)))
.issuer("test-issuer")
Expand Down
Loading

0 comments on commit 2393fdf

Please sign in to comment.