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

fix: make scope parameter of PresentationQuery optional #388

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 @@ -33,6 +33,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;
Expand All @@ -53,52 +54,69 @@ public CredentialQueryResolverImpl(CredentialStore credentialStore, ScopeToCrite
}

@Override
public QueryResult query(String participantContextId, PresentationQueryMessage query, List<String> issuerScopes) {
public QueryResult query(String participantContextId, PresentationQueryMessage query, List<String> accessTokenScopes) {
if (query.getPresentationDefinition() != null) {
throw new UnsupportedOperationException("Querying with a DIF Presentation Exchange definition is not yet supported.");
}
if (query.getScopes().isEmpty()) {
return QueryResult.noScopeFound("Invalid query: must contain at least one scope.");
var requestedScopes = query.getScopes();
// check that all access token scopes are valid
var accessTokenScopesParseResult = parseScopes(accessTokenScopes);
if (accessTokenScopesParseResult.failed()) {
return QueryResult.invalidScope(accessTokenScopesParseResult.getFailureMessages());
}

// check that all prover scopes are valid
var proverScopeResult = parseScopes(query.getScopes());
if (proverScopeResult.failed()) {
return QueryResult.invalidScope(proverScopeResult.getFailureMessages());
// fetch all credentials according to the scopes in the access token:
var allowedScopes = accessTokenScopesParseResult.getContent();

if (allowedScopes.isEmpty()) {
// no scopes granted, no scopes requested, return empty list
if (requestedScopes.isEmpty()) {
return QueryResult.success(Stream.empty());
}
// no scopes granted, but some requested -> unauthorized! This is a shortcut to save some database communication
var msg = "Permission was not granted on any credentials (empty access token scope list), but %d were requested.".formatted(requestedScopes.size());
monitor.warning(msg);
QueryResult.unauthorized(msg.formatted(requestedScopes.size()));
}

// check that all issuer scopes are valid
var issuerScopeResult = parseScopes(issuerScopes);
if (issuerScopeResult.failed()) {
return QueryResult.invalidScope(issuerScopeResult.getFailureMessages());
}

// query storage for requested credentials
var credentialResult = queryCredentials(proverScopeResult.getContent(), participantContextId);
if (credentialResult.failed()) {
return QueryResult.storageFailure(credentialResult.getFailureMessages());
}

// the credentials requested by the other party
var requestedCredentials = credentialResult.getContent();

// check that prover scope is not wider than issuer scope
var allowedCred = queryCredentials(issuerScopeResult.getContent(), participantContextId);
var allowedCred = queryCredentials(allowedScopes, participantContextId);
if (allowedCred.failed()) {
return QueryResult.invalidScope(allowedCred.getFailureMessages());
return QueryResult.storageFailure(allowedCred.getFailureMessages());
}

// now narrow down the requested credentials to only contain allowed credentials
var content = allowedCred.getContent();
var isValidQuery = new HashSet<>(content.stream().map(VerifiableCredentialResource::getId).toList())
.containsAll(requestedCredentials.stream().map(VerifiableCredentialResource::getId).toList());

var allowedCredentials = allowedCred.getContent();
Stream<VerifiableCredentialResource> credentialResult;

// the client did not request any scopes, so we simply return all they have access to
if (requestedScopes.isEmpty()) {
credentialResult = allowedCredentials.stream();
} else {
// check that all prover scopes are valid
var requestedScopesParseResult = parseScopes(requestedScopes);
if (requestedScopesParseResult.failed()) {
return QueryResult.invalidScope(requestedScopesParseResult.getFailureMessages());
}
// query storage for requested credentials
var requestedCredentialResult = queryCredentials(requestedScopesParseResult.getContent(), participantContextId);
if (requestedCredentialResult.failed()) {
return QueryResult.storageFailure(requestedCredentialResult.getFailureMessages());
}
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.");
}

credentialResult = requestedCredentials.stream();
}
// filter out any expired, revoked or suspended credentials
return isValidQuery ?
QueryResult.success(requestedCredentials.stream()
.filter(this::filterInvalidCredentials) // we still have to filter invalid creds, b/c a revocation may not have been detected yet
.map(VerifiableCredentialResource::getVerifiableCredential))
: QueryResult.unauthorized("Invalid query: requested Credentials outside of scope.");
return QueryResult.success(credentialResult
.filter(this::filterInvalidCredentials)
.map(VerifiableCredentialResource::getVerifiableCredential));
}

private boolean filterInvalidCredentials(VerifiableCredentialResource verifiableCredentialResource) {
Expand Down Expand Up @@ -140,7 +158,6 @@ private Result<List<Criterion>> parseScopes(List<String> scopes) {
return success(transformResult.stream().map(AbstractResult::getContent).toList());
}


private Result<Collection<VerifiableCredentialResource>> queryCredentials(List<Criterion> criteria, String participantContextId) {
var results = criteria.stream()
.map(criterion -> convertToQuerySpec(criterion, participantContextId))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,46 @@ void query_noResult() {
}

@Test
void query_noProverScope_shouldReturnEmpty() {
void query_invalidAccessTokenScope_shouldReturnEmpty() {
when(storeMock.query(any())).thenReturn(success(Collections.emptyList()));
var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery(), List.of("foobar"));
assertThat(res.succeeded()).isFalse();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE);
assertThat(res.getFailureDetail()).contains("Invalid query: must contain at least one scope.");
assertThat(res.getFailureDetail()).contains("Scope string cannot be converted: Scope string has invalid format.");
}

@Test
void query_proverScopeStringInvalid_shouldReturnFailure() {
void query_noAccessTokenScope_noQueryScope_shouldReturnEmpty() {
when(storeMock.query(any())).thenReturn(success(Collections.emptyList()));
var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery(/*empty scopes*/), List.of());
assertThat(res.succeeded()).isTrue();
assertThat(res.getContent()).isEmpty();
}

@Test
void query_noQueryScope_shouldAllPermitted() {
var credential = createCredentialResource("AnotherCredential");
when(storeMock.query(any())).thenReturn(success(List.of(credential)));

var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery(/*empty scopes*/), List.of("org.eclipse.edc.vc.type:AnotherCredential:read"));
assertThat(res.succeeded()).isTrue();
assertThat(res.getContent()).usingRecursiveFieldByFieldElementComparator().containsExactly(credential.getVerifiableCredential());
}

@Test
void query_noAccessTokenScope_withQueryScope_shouldReturnFailure() {
var credential = createCredentialResource("AnotherCredential");
when(storeMock.query(any()))
.thenReturn(success(List.of(credential)));

var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery("org.eclipse.edc.vc.type:AnotherCredential:read"), List.of());
assertThat(res.succeeded()).isFalse();
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.");
}

@Test
void query_accessTokenScopeStringInvalid_shouldReturnFailure() {
when(storeMock.query(any())).thenReturn(success(Collections.emptyList()));
var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID,
createPresentationQuery("invalid"), List.of("org.eclipse.edc.vc.type:AnotherCredential:read"));
Expand All @@ -98,7 +128,7 @@ void query_scopeStringHasWrongOperator_shouldReturnFailure() {
var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:write"), List.of("ignored"));
assertThat(res.failed()).isTrue();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE);
assertThat(res.getFailureDetail()).contains("Invalid scope operation: write");
assertThat(res.getFailureDetail()).contains("Scope string cannot be converted: Scope string has invalid format.");
}

@Test
Expand All @@ -117,14 +147,14 @@ void query_verifyDifferentObjects() {
var credential2 = createCredentialResource(createCredential("TestCredential").build()).id("id1").build();

when(storeMock.query(any()))
.thenAnswer(i -> success(List.of(credential1)))
.thenAnswer(i -> success(List.of(credential2)));
.thenReturn(success(List.of(credential1)))
.thenReturn(success(List.of(credential2)));

var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID,
createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read"));

assertThat(res.succeeded()).withFailMessage(res::getFailureDetail).isTrue();
assertThat(res.getContent()).usingRecursiveFieldByFieldElementComparator().containsExactly(credential1.getVerifiableCredential());
assertThat(res.getContent()).usingRecursiveFieldByFieldElementComparator().containsExactly(credential2.getVerifiableCredential());
}

@Test
Expand Down Expand Up @@ -167,14 +197,16 @@ void query_presentationDefinition_unsupported() {
void query_requestsTooManyCredentials_shouldReturnFailure() {
var credential1 = createCredentialResource("TestCredential");
var credential2 = createCredentialResource("AnotherCredential");
when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1, credential2)))
.thenAnswer(i -> success(List.of(credential1)));
when(storeMock.query(any()))
.thenReturn(success(List.of(credential1)))
.thenReturn(success(List.of(credential2)))
.thenReturn(success(List.of(credential1)));

var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID,
createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read",
"org.eclipse.edc.vc.type:AnotherCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read"));

assertThat(res.failed()).isTrue();
assertThat(res.succeeded()).isFalse();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE);
assertThat(res.getFailureDetail()).isEqualTo("Invalid query: requested Credentials outside of scope.");
}
Expand Down Expand Up @@ -224,11 +256,15 @@ void query_sameSizeDifferentScope() {
var credential2 = createCredentialResource("AnotherCredential");
var credential3 = createCredentialResource("FooCredential");
var credential4 = createCredentialResource("BarCredential");
when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1, credential2)))
.thenAnswer(i -> success(List.of(credential3, credential4)));
when(storeMock.query(any()))
.thenReturn(success(List.of(credential1)))
.thenReturn(success(List.of(credential2)))
.thenReturn(success(List.of(credential3)))
.thenReturn(success(List.of(credential4)));

var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID,
createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read"), List.of("org.eclipse.edc.vc.type:FooCredential:read", "org.eclipse.edc.vc.type:BarCredential:read"));
createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read"),
List.of("org.eclipse.edc.vc.type:FooCredential:read", "org.eclipse.edc.vc.type:BarCredential:read"));

assertThat(res.succeeded()).isFalse();
assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE);
Expand Down
Loading