Skip to content

Commit

Permalink
feat: add get-request endpoint for holder requests (#615)
Browse files Browse the repository at this point in the history
* feat: add get-request endpoint for holder requests

* remove param
  • Loading branch information
paullatzelsperger authored Feb 25, 2025
1 parent e2caa02 commit 39c025c
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.eclipse.edc.transaction.spi.TransactionContext;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.time.Instant;
Expand Down Expand Up @@ -97,6 +98,11 @@ public ServiceResult<String> initiateRequest(String participantContextId, String
return ServiceResult.success(holderPid);
}

@Override
public @Nullable HolderCredentialRequest findById(String holderPid) {
return transactionContext.execute(() -> store.findById(holderPid));
}

@Override
protected StateMachineManager.Builder configureStateMachineManager(StateMachineManager.Builder builder) {
return builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer;
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderRequestState;
import org.eclipse.edc.identityhub.spi.credential.request.store.HolderCredentialRequestStore;
import org.eclipse.edc.identityhub.spi.did.store.DidResourceStore;
Expand All @@ -37,7 +38,7 @@
import org.eclipse.edc.spi.query.QuerySpec;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.sql.testfixtures.PostgresqlEndToEndExtension;
import org.hamcrest.Matchers;
import org.eclipse.edc.transaction.spi.TransactionContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
Expand All @@ -56,6 +57,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.eclipse.edc.util.io.Ports.getFreePort;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
Expand Down Expand Up @@ -263,14 +265,15 @@ void createCredentialRequest(IdentityHubEndToEndTestContext context, HolderCrede
context.createSuperUser();
var user = "user1";
var token = context.createParticipant(user);
var holderPid = UUID.randomUUID().toString();
var request =
"""
{
"issuerDid": "did:web:issuer",
"holderPid": "test-request-id",
"holderPid": "%s",
"credentials": [{ "format": "VC1_0_JWT", "credentialType": "TestCredential"}]
}
""";
""".formatted(holderPid);
context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", token))
Expand All @@ -279,23 +282,93 @@ void createCredentialRequest(IdentityHubEndToEndTestContext context, HolderCrede
.then()
.log().ifValidationFails()
.statusCode(201)
.body(Matchers.equalTo("test-request-id"));
.body(equalTo(holderPid));

// wait until the state machine has progress to the REQUESTED state
await().pollInterval(Duration.ofSeconds(1))
.atMost(Duration.ofSeconds(10))
.untilAsserted(() -> {
var requests = store.query(QuerySpec.max());
assertThat(requests).hasSize(1)
.allSatisfy(t -> {
assertThat(t.getState()).isEqualTo(HolderRequestState.REQUESTED.code());
assertThat(t.getIssuerPid()).isEqualTo(issuerPid);
assertThat(t.getHolderPid()).isEqualTo("test-request-id");
});
var result = store.findById(holderPid);
assertThat(result).isNotNull();
assertThat(result.getState()).isEqualTo(HolderRequestState.REQUESTED.code());
assertThat(result.getIssuerPid()).isEqualTo(issuerPid);
assertThat(result.getHolderPid()).isEqualTo(holderPid);
});
}
}

@Test
void getRequest_success(IdentityHubEndToEndTestContext context, TransactionContext trx, HolderCredentialRequestStore store) {
var userId = "user1";
var token = context.createParticipant(userId);

var holderPid = UUID.randomUUID().toString();
var holderRequest = HolderCredentialRequest.Builder.newInstance()
.id(holderPid)
.participantContextId(userId)
.issuerDid("did:web:issuer")
.issuerPid("dummy-issuance-id")
.credentialType("TestCredential", CredentialFormat.VC2_0_JOSE.toString())
.build();

trx.execute(() -> store.save(holderRequest));

context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", token))
.get("/v1alpha/participants/%s/credentials/request/%s".formatted(toBase64(userId), holderPid))
.then()
.log().ifValidationFails()
.statusCode(200)
.body("holderPid", equalTo(holderPid))
.body("issuerDid", equalTo("did:web:issuer"))
.body("issuerPid", equalTo("dummy-issuance-id"))
.body("status", equalTo("CREATED"));
}

@Test
void getRequest_notAuthorized_returns403(IdentityHubEndToEndTestContext context, HolderCredentialRequestStore store, TransactionContext trx) {
var user1 = "user1";
var user2 = "user2";
var token1 = context.createParticipant(user1);
var token2 = context.createParticipant(user2);

var holderPid = UUID.randomUUID().toString();
var holderRequest = HolderCredentialRequest.Builder.newInstance()
.id(holderPid)
.participantContextId(user1)
.issuerDid("did:web:issuer")
.issuerPid("dummy-issuance-id")
.credentialType("TestCredential", CredentialFormat.VC2_0_JOSE.toString())
.build();

trx.execute(() -> store.save(holderRequest));

context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", token2)) // user 2 tries to access credential request status for user 1 -> not allowed!
.get("/v1alpha/participants/%s/credentials/request/%s".formatted(toBase64(user1), holderPid))
.then()
.log().ifValidationFails()
.statusCode(403);
}

@Test
void getRequest_whenNotFound_shouldReturn404(IdentityHubEndToEndTestContext context, HolderCredentialRequestStore store, TransactionContext trx) {
var userId = "user1";
var token = context.createParticipant(userId);

var holderPid = UUID.randomUUID().toString();

context.getIdentityApiEndpoint().baseRequest()
.contentType(JSON)
.header(new Header("x-api-key", token))
.get("/v1alpha/participants/%s/credentials/request/%s".formatted(toBase64(userId), holderPid))
.then()
.log().ifValidationFails()
.statusCode(404);
}

private String toBase64(String s) {
return Base64.getUrlEncoder().encodeToString(s.getBytes());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"version": "1.0.0-alpha",
"urlPath": "/v1alpha",
"lastUpdated": "2025-02-21T14:00:00Z",
"lastUpdated": "2025-02-24T14:00:00Z",
"maturity": null
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.model.CredentialRequestDto;
import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.model.HolderCredentialRequestDto;
import org.eclipse.edc.identityhub.spi.authorization.AuthorizationService;
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.CredentialRequestManager;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest;
Expand Down Expand Up @@ -178,18 +179,19 @@ public Response requestCredential(@PathParam("participantContextId") String part
}

@GET
@Path("/request/{issuerPid}")
@Path("/request/{holderPid}")
@Override
public HolderCredentialRequestDto getCredentialRequest(@PathParam("participantContextId") String participantContextId,
@PathParam("issuerPid") String issuerPid,
@PathParam("holderPid") String holderPid,
@Context SecurityContext securityContext) {

var participantId = onEncoded(participantContextId).orElseThrow(InvalidRequestException::new);
authorizationService.isAuthorized(securityContext, participantId, ParticipantContext.class)
.orElseThrow(exceptionMapper(ParticipantContext.class, participantId));

//todo: implement CredentialRequestService.fetchCredentialStatus from DCP issuer
return new HolderCredentialRequestDto("did:web:issuer", "dummy-request-id", "issuance-process-ic", "ISSUED", List.of(UUID.randomUUID().toString()), List.of());
return ofNullable(credentialRequestService.findById(holderPid))
.map(req -> new HolderCredentialRequestDto(req.getIssuerDid(), req.getHolderPid(), req.getIssuerPid(), req.stateAsString(), List.of(), req.getTypesAndFormats()))
.orElseThrow(() -> new ObjectNotFoundException(HolderCredentialRequest.class, holderPid));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

package org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.model;

import java.util.List;
import java.util.Collection;
import java.util.Map;

/**
* Represents a credential request of the holder.
Expand All @@ -24,9 +25,9 @@
* @param issuerPid the process ID returned from the issuer
* @param status REQUESTED, ISSUED, etc.
* @param credentialIds after the credentials are issued, their IDs are stored here
* @param credentialTypes list of credential types/formats that were originally requested
* @param typesAndFormats list of credential types/formats that were originally requested
*/
// todo: this DTO might get removed again later, when we have a HolderCredentialRequest entity, which will likely have the same signature
public record HolderCredentialRequestDto(String issuerDid, String holderPid, String issuerPid, String status,
List<String> credentialIds, List<CredentialDescriptor> credentialTypes) {
Collection<String> credentialIds,
Map<String, String> typesAndFormats) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.model.CredentialDescriptor;
import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.model.CredentialRequestDto;
import org.eclipse.edc.identityhub.spi.authorization.AuthorizationService;
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.CredentialRequestManager;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest;
Expand Down Expand Up @@ -55,6 +56,7 @@
import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.ServiceResult.unauthorized;
import static org.eclipse.edc.spi.result.StoreResult.alreadyExists;
import static org.hamcrest.Matchers.aMapWithSize;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.ArgumentMatchers.any;
Expand Down Expand Up @@ -519,4 +521,34 @@ void whenNotAuthorized_returns403() {
verifyNoInteractions(credentialRequestService);
}
}

@Nested
class GetCredentialRequest {
@Test
void success() {
when(credentialRequestService.findById(any()))
.thenReturn(HolderCredentialRequest.Builder.newInstance()
.issuerDid("did:web:issuer")
.participantContextId("test-participant")
.typesAndFormats(Map.of("TestCredential", CredentialFormat.VC1_0_JWT.toString()))
.build());

baseRequest()
.get("/request/test-issuer-pid")
.then()
.log().ifValidationFails()
.statusCode(200)
.body("issuerDid", equalTo("did:web:issuer"))
.body("typesAndFormats", aMapWithSize(1));
}

@Test
void getRequest_whenNotFound_shouldReturn20xNoBody() {
baseRequest()
.get("/request/test-issuer-pid")
.then()
.log().ifValidationFails()
.statusCode(404);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

package org.eclipse.edc.identityhub.store.sql.credentialrequest.schema;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
import org.eclipse.edc.identityhub.spi.credential.request.store.HolderCredentialRequestStore;
Expand Down Expand Up @@ -47,8 +46,6 @@
*/
public class SqlHolderCredentialRequestStore extends AbstractSqlStore implements HolderCredentialRequestStore {

private static final TypeReference<List<String>> CREDENTIALTYPES_LIST_REF = new TypeReference<>() {
};
private final String leaseHolderName;
private final SqlLeaseContextBuilder leaseContext;

Expand Down
1 change: 1 addition & 0 deletions spi/verifiable-credential-spi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ plugins {

dependencies {

api(project(":spi:holder-credential-request-spi"))
api(project(":spi:participant-context-spi"))
api(libs.edc.spi.dcp)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@

package org.eclipse.edc.identityhub.spi.verifiablecredentials;

import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
import org.eclipse.edc.runtime.metamodel.annotation.ExtensionPoint;
import org.eclipse.edc.spi.entity.StateEntityManager;
import org.eclipse.edc.spi.result.ServiceResult;
import org.jetbrains.annotations.Nullable;

import java.util.Map;

Expand All @@ -33,11 +35,19 @@ public interface CredentialRequestManager extends StateEntityManager {
/**
* Initiates the holder-side credential request by sending the DCP message to the issuer
*
* @param participantContext The Participant Context ID of the requestor
* @param issuerDid The DID of the issuer
* @param holderPid The holder-defined request ID.
* @param typesAndFormats A map containing credential-type - credential-format entries
* @param participantContextId The Participant Context ID of the requestor
* @param issuerDid The DID of the issuer
* @param holderPid The holder-defined request ID.
* @param typesAndFormats A map containing credential-type - credential-format entries
* @return A ServiceResult containing the database ID of the {@code HolderCredentialRequest}.
*/
ServiceResult<String> initiateRequest(String participantContext, String issuerDid, String holderPid, Map<String, String> typesAndFormats);
ServiceResult<String> initiateRequest(String participantContextId, String issuerDid, String holderPid, Map<String, String> typesAndFormats);

/**
* Finds a {@link HolderCredentialRequest} for the given participant context, with the given ID
*
* @param holderPid The (holder-side) ID of the request. Not to be confused with issuerPid.
* @return the holder request, or null if not found
*/
@Nullable HolderCredentialRequest findById(String holderPid);
}

0 comments on commit 39c025c

Please sign in to comment.