Skip to content

Commit

Permalink
fix: make scope parameter of PresentationQuery optional
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Jul 3, 2024
1 parent ff1a77d commit dd8376a
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 50 deletions.
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

0 comments on commit dd8376a

Please sign in to comment.