Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,18 @@ public QueryResult query(String participantContextId, PresentationQueryMessage q
}
var requestedCredentials = requestedCredentialResult.getContent();

// clients can never request more credentials than they are permitted to, i.e. their scope list can not exceed the scopes taken
// from the access token
var isValidQuery = new HashSet<>(allowedCredentials.stream().map(VerifiableCredentialResource::getId).toList())
.containsAll(requestedCredentials.stream().map(VerifiableCredentialResource::getId).toList());

if (!isValidQuery) {
return QueryResult.unauthorized("Invalid query: requested Credentials outside of scope.");
// the DCP spec requires that only those credentials are returned that the client is eligible for. This check
// checks whether the client has requested credentials outside their permitted scopes
var allowedIds = new HashSet<>(allowedCredentials.stream().map(VerifiableCredentialResource::getId).toList());
var hasRequestedMore = !allowedIds.containsAll(requestedCredentials.stream().map(VerifiableCredentialResource::getId).toList());

if (hasRequestedMore) {
var allowedTypes = allowedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential).map(vc -> vc.credential().getType()).flatMap(List::stream).distinct().toList();
var requestedTypes = requestedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential).map(vc -> vc.credential().getType()).flatMap(List::stream).distinct().toList();
monitor.debug("Client has requested more credentials than allowed. Allowed credentials: %s. Requested credentials: %s".formatted(allowedTypes, requestedTypes));
}

credentialResult = requestedCredentials.stream();
credentialResult = requestedCredentials.stream().filter(c -> allowedIds.contains(c.getId()));
}
// filter out any expired, revoked or suspended credentials
return QueryResult.success(credentialResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.json.JsonObject;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject;
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.HolderRequestState;
import org.eclipse.edc.identityhub.spi.credential.request.store.HolderCredentialRequestStore;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.generator.CredentialWriteRequest;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.generator.CredentialWriter;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.CredentialProfile;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.store.CredentialStore;
Expand All @@ -35,7 +35,6 @@

import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
Expand All @@ -47,7 +46,7 @@


public class CredentialWriterImpl implements CredentialWriter {
private static final List<String> VALID_CREDENTIAL_FORMATS = Arrays.stream(CredentialFormat.values()).map(Object::toString).toList();

private static final List<HolderRequestState> ALLOWED_STATES = List.of(REQUESTED, ISSUED);
private final CredentialStore credentialStore;
private final TypeTransformerRegistry credentialTransformerRegistry;
Expand Down Expand Up @@ -120,11 +119,11 @@ public ServiceResult<Void> write(String holderPid, String issuerPid, Collection<

private ServiceResult<VerifiableCredentialResource> convertToResource(CredentialWriteRequest credentialWriteRequest, String participantContextId) {

CredentialFormat credentialFormat;
try {
credentialFormat = CredentialFormat.valueOf(credentialWriteRequest.credentialFormat().toUpperCase());
} catch (IllegalArgumentException e) {
return ServiceResult.badRequest(String.format("Invalid format: '%s', expected one of %s".formatted(credentialWriteRequest.credentialFormat(), VALID_CREDENTIAL_FORMATS)));
var profile = credentialWriteRequest.credentialFormat();
var mappedFormat = CredentialProfile.formatForProfile(profile);

if (mappedFormat.failed()) {
return mappedFormat.mapFailure();
}

//attempt to convert the raw credential to JSON -> would mean LD, or JWT otherwise
Expand All @@ -137,7 +136,7 @@ private ServiceResult<VerifiableCredentialResource> convertToResource(Credential
}
var credential = transformationResult.getContent();

var container = new VerifiableCredentialContainer(credentialWriteRequest.rawCredential(), credentialFormat, credential);
var container = new VerifiableCredentialContainer(credentialWriteRequest.rawCredential(), mappedFormat.getContent(), credential);

var resource = VerifiableCredentialResource.Builder.newHolder()
.credential(container)
Expand All @@ -152,6 +151,7 @@ private ServiceResult<VerifiableCredentialResource> convertToResource(Credential
return ServiceResult.success(resource);
}


private Optional<JsonObject> tryConvertToJson(@NotNull String rawCredential) {
try {
return Optional.of(objectMapper.readValue(rawCredential, JsonObject.class));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.eclipse.edc.spi.result.StoreResult.success;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand Down Expand Up @@ -111,9 +112,8 @@ void query_noAccessTokenScope_withQueryScope_shouldReturnFailure() {
.thenReturn(success(List.of(credential)));

var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery("org.eclipse.dspace.dcp.vc.type:AnotherCredential:read"), List.of());
assertThat(res).isFailed();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE);
verify(monitor).warning("Permission was not granted on any credentials (empty access token scope list), but 1 were requested.");
assertThat(res).isSucceeded();
assertThat(res.getContent()).isEmpty();
}

@Test
Expand Down Expand Up @@ -210,20 +210,27 @@ void query_presentationDefinition_unsupported() {

@Test
void query_requestsTooManyCredentials_shouldReturnFailure() {
var credential1 = createCredentialResource("TestCredential");
var credential2 = createCredentialResource("AnotherCredential");
when(storeMock.query(any()))
.thenReturn(success(List.of(credential1)))
.thenReturn(success(List.of(credential2)))
var credential1 = createCredentialResource("TestCredential_1");
var credential2 = createCredentialResource("TestCredential_2");
var credential3 = createCredentialResource("TestCredential_3");
when(storeMock.query(argThat(q -> q != null && q.getFilterExpression().stream().anyMatch(c -> c.getOperandRight().toString().contains("TestCredential_1")))))
.thenReturn(success(List.of(credential1)));

when(storeMock.query(argThat(q -> q != null && q.getFilterExpression().stream().anyMatch(c -> c.getOperandRight().toString().contains("TestCredential_2")))))
.thenReturn(success(List.of(credential2)));

when(storeMock.query(argThat(q -> q != null && q.getFilterExpression().stream().anyMatch(c -> c.getOperandRight().toString().contains("TestCredential_3")))))
.thenReturn(success(List.of(credential3)));


var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID,
createPresentationQuery("org.eclipse.dspace.dcp.vc.type:TestCredential:read",
"org.eclipse.dspace.dcp.vc.type:AnotherCredential:read"), List.of("org.eclipse.dspace.dcp.vc.type:TestCredential:read"));
createPresentationQuery("org.eclipse.dspace.dcp.vc.type:TestCredential_1:read", "org.eclipse.dspace.dcp.vc.type:TestCredential_2:read", "org.eclipse.dspace.dcp.vc.type:TestCredential_3:read"),
List.of("org.eclipse.dspace.dcp.vc.type:TestCredential_1:read", "org.eclipse.dspace.dcp.vc.type:TestCredential_2:read"));

assertThat(res).isFailed();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE);
assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope.");
assertThat(res).isSucceeded();
var credentials = res.getContent().toList();
assertThat(credentials).hasSize(2)
.containsExactlyInAnyOrder(credential1.getVerifiableCredential(), credential2.getVerifiableCredential());
}

@Test
Expand Down Expand Up @@ -260,9 +267,7 @@ void query_requestedCredentialNotAllowed() {
var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID,
createPresentationQuery("org.eclipse.dspace.dcp.vc.type:TestCredential:read"), List.of("org.eclipse.dspace.dcp.vc.type:AnotherCredential:read"));

assertThat(res.failed()).isTrue();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE);
assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope.");
assertThat(res).isSucceeded().satisfies(creds -> assertThat(creds).isEmpty());
}

@Test
Expand All @@ -281,9 +286,8 @@ void query_sameSizeDifferentScope() {
createPresentationQuery("org.eclipse.dspace.dcp.vc.type:TestCredential:read", "org.eclipse.dspace.dcp.vc.type:AnotherCredential:read"),
List.of("org.eclipse.dspace.dcp.vc.type:FooCredential:read", "org.eclipse.dspace.dcp.vc.type:BarCredential:read"));

assertThat(res).isFailed();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE);
assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope.");
assertThat(res).isSucceeded()
.satisfies(creds -> assertThat(creds).isEmpty());
}

@Test
Expand Down Expand Up @@ -350,6 +354,10 @@ void query_whenRevokedCredential_doesNotInclude() {
verify(monitor).warning(eq("Credential '%s' not valid: revoked".formatted(credential.getId())));
}

private QuerySpec queryingFor(String slug) {
return argThat(q -> q.getFilterExpression().stream().anyMatch(c -> c.getOperandRight().toString().contains(slug)));
}

private VerifiableCredentialResource.Builder createCredentialResource(VerifiableCredential cred) {
return VerifiableCredentialResource.Builder.newHolder()
.credential(new VerifiableCredentialContainer("foobar", CredentialFormat.VC1_0_LD, cred))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ void query_proofOfPossessionFails_shouldReturn401(IdentityHub identityHub) throw
}

@Test
void query_credentialQueryResolverFails_shouldReturn403(IdentityHub identityHub, CredentialStore store) throws JOSEException, JsonProcessingException {
void query_moreScopesThanAccessToken_shouldOnlyReturnPermittedCreds(IdentityHub identityHub, CredentialStore store) throws JOSEException, JsonProcessingException {

var token = generateSiToken();

Expand All @@ -313,16 +313,27 @@ void query_credentialQueryResolverFails_shouldReturn403(IdentityHub identityHub,
storeCredential(VC_EXAMPLE_2, CredentialFormat.VC1_0_JWT, store);


identityHub.getCredentialsEndpoint().baseRequest()
var response = identityHub.getCredentialsEndpoint().baseRequest()
.contentType(JSON)
.header(AUTHORIZATION, "Bearer " + token)
.body(VALID_QUERY_WITH_ADDITIONAL_SCOPE)
.post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID))
.then()
.log().ifError()
.statusCode(403)
.body("[0].type", equalTo("NotAuthorized"))
.body("[0].message", equalTo("Invalid query: requested Credentials outside of scope."));
.statusCode(200)
.extract().body().as(JsonObject.class);

assertThat(response)
.hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage"))
.hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(1))
.hasEntrySatisfying("presentation", jsonValue -> {
assertThat(vpTokensExtractor(jsonValue)).hasSize(1)
.first()
.satisfies(vpToken -> {
assertThat(vpToken).isNotNull();
assertThat(extractCredentials(vpToken)).hasSize(1).allSatisfy(vc -> assertThat(vc.getType()).contains("VerifiableCredential", "AlumniCredential"));
});
});
}

@Test
Expand Down Expand Up @@ -803,7 +814,19 @@ private List<VerifiableCredential> extractCredentials(String vpToken) {

Map<String, Object> map = (Map<String, Object>) OBJECT_MAPPER.convertValue(vpClaim, Map.class);

return (List<VerifiableCredential>) map.get("verifiableCredential");
var credentials = (List<Object>) map.get("verifiableCredential");
return credentials.stream().map(s -> {
if (s instanceof String json) {

try {
return OBJECT_MAPPER.readValue(json, VerifiableCredential.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
} else {
return OBJECT_MAPPER.convertValue(s, VerifiableCredential.class);
}
}).toList();

} catch (ParseException e) {
throw new RuntimeException(e);
Expand Down
4 changes: 2 additions & 2 deletions e2e-tests/tck-tests/presentation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ dependencies {
testImplementation(libs.restAssured)
testImplementation(libs.junit.jupiter.api)

testImplementation(libs.dcp.tck.runtime)
testImplementation(libs.tck.runtime)
testImplementation(libs.dcp.system)
testImplementation(libs.dsp.core)
testImplementation(libs.tck.core)
testImplementation(testFixtures(project(":e2e-tests:identityhub-test-fixtures")))
testImplementation(libs.junit.platform.launcher)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ void runIssuanceFlowTests(IdentityHub identityHub) throws InterruptedException {

var response = createParticipant(identityHub, baseCredentialServiceUrl);

try (var tckContainer = new GenericContainer<>("eclipsedataspacetck/dcp-tck-runtime:1.0.0-RC3")
try (var tckContainer = new GenericContainer<>("eclipsedataspacetck/dcp-tck-runtime:1.0.0-RC7")
.withExtraHost("host.docker.internal", "host-gateway")
.withExposedPorts(CALLBACK_PORT)
.withEnv(Map.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ void runIssuanceFlowTests(IssuerService issuer) throws InterruptedException {
var response = createParticipantContext(issuer, baseIssuerServiceUrl);
createDefinitions(issuer);

try (var tckContainer = new GenericContainer<>("eclipsedataspacetck/dcp-tck-runtime:1.0.0-RC3")
try (var tckContainer = new GenericContainer<>("eclipsedataspacetck/dcp-tck-runtime:1.0.0-RC7")
.withExtraHost("host.docker.internal", "host-gateway")
.withExposedPorts(CALLBACK_PORT)
.withEnv(Map.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ void runPresentationFlowTestsDocker(IdentityHub identityHub) throws InterruptedE

var response = createParticipant(identityHub, baseCredentialServiceUrl);

try (var tckContainer = new GenericContainer<>("eclipsedataspacetck/dcp-tck-runtime:1.0.0-RC5")
try (var tckContainer = new GenericContainer<>("eclipsedataspacetck/dcp-tck-runtime:1.0.0-RC7")
.withExtraHost("host.docker.internal", "host-gateway")
.withExposedPorts(CALLBACK_PORT)
.withEnv(Map.of(
Expand Down
6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ format.version = "1.1"
awaitility = "4.3.0"
bouncyCastle-jdk18on = "1.84"
edc = "0.18.0-SNAPSHOT"
dcp-tck = "1.0.0-RC6"
dcp-tck = "1.0.0-RC7"
jackson = "2.21"
jakarta-annotation = "3.0.0"
jersey = "4.0.2"
Expand Down Expand Up @@ -122,8 +122,8 @@ opentelemetry-sdk = { module = "io.opentelemetry:opentelemetry-sdk", version.ref

# DCP-TCK libraries
dcp-testcases = { module = "org.eclipse.dataspacetck.dcp:dcp-testcases", version.ref = "dcp-tck" }
dcp-tck-runtime = { module = "org.eclipse.dataspacetck.dsp:tck-runtime", version.ref = "dcp-tck" }
dsp-core = { module = "org.eclipse.dataspacetck.dsp:core", version.ref = "dcp-tck" }
tck-runtime = { module = "org.eclipse.dataspacetck.common:tck-runtime", version.ref = "dcp-tck" }
tck-core = { module = "org.eclipse.dataspacetck.common:core", version.ref = "dcp-tck" }
dcp-system = { module = "org.eclipse.dataspacetck.dcp:dcp-system", version.ref = "dcp-tck" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.eclipse.edc.iam.decentralizedclaims.spi.CredentialServiceUrlResolver;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer;
import org.eclipse.edc.identityhub.spi.authentication.ParticipantSecureTokenService;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.CredentialProfile;
import org.eclipse.edc.issuerservice.spi.holder.model.Holder;
import org.eclipse.edc.issuerservice.spi.holder.store.HolderStore;
import org.eclipse.edc.issuerservice.spi.issuance.delivery.CredentialStorageClient;
Expand Down Expand Up @@ -147,7 +148,7 @@ private JsonObject createCredentialMessage(IssuanceProcess issuanceProcess, Coll
private JsonObject toJson(VerifiableCredentialContainer credential) {
return Json.createObjectBuilder()
.add("credentialType", credential.credential().getType().stream().filter("VerifiableCredential"::equals).findFirst().orElseThrow())
.add("format", credential.format().name())
.add("format", CredentialProfile.profileForFormat(credential.format()).orElseThrow(f -> new IllegalArgumentException("Invalid credential format: " + credential.format())))
.add("payload", credential.rawVc())
.build();
}
Expand Down
Loading
Loading