-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement CredentialQueryResolver (#168)
* add possibility to supply PublicKey as config value * added QueryResolverImpl + Test * add storageresult class * made scope-to-criterion conversion pluggable * cleanup * javadoc * DEPENDENCIES * Update core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java Co-authored-by: Jim Marino <[email protected]> * pr remars --------- Co-authored-by: Jim Marino <[email protected]>
- Loading branch information
1 parent
d47d56d
commit 9f0a2f8
Showing
15 changed files
with
674 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 130 additions & 0 deletions
130
...-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/* | ||
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) | ||
* | ||
* This program and the accompanying materials are made available under the | ||
* terms of the Apache License, Version 2.0 which is available at | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* Contributors: | ||
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation | ||
* | ||
*/ | ||
|
||
package org.eclipse.edc.identityhub.core; | ||
|
||
import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; | ||
import org.eclipse.edc.identityhub.spi.model.PresentationQuery; | ||
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; | ||
import org.eclipse.edc.identityhub.spi.resolution.QueryResult; | ||
import org.eclipse.edc.identityhub.spi.store.CredentialStore; | ||
import org.eclipse.edc.identityhub.spi.store.model.VerifiableCredentialResource; | ||
import org.eclipse.edc.spi.query.Criterion; | ||
import org.eclipse.edc.spi.query.QuerySpec; | ||
import org.eclipse.edc.spi.result.AbstractResult; | ||
import org.eclipse.edc.spi.result.Result; | ||
|
||
import java.util.List; | ||
import java.util.function.Predicate; | ||
|
||
import static org.eclipse.edc.spi.result.Result.failure; | ||
import static org.eclipse.edc.spi.result.Result.success; | ||
|
||
|
||
public class CredentialQueryResolverImpl implements CredentialQueryResolver { | ||
|
||
private final CredentialStore credentialStore; | ||
private final ScopeToCriterionTransformer scopeTransformer; | ||
|
||
public CredentialQueryResolverImpl(CredentialStore credentialStore, ScopeToCriterionTransformer scopeTransformer) { | ||
this.credentialStore = credentialStore; | ||
this.scopeTransformer = scopeTransformer; | ||
} | ||
|
||
@Override | ||
public QueryResult query(PresentationQuery query, List<String> issuerScopes) { | ||
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."); | ||
} | ||
|
||
// check that all prover scopes are valid | ||
var proverScopeResult = parseScopes(query.getScopes()); | ||
if (proverScopeResult.failed()) { | ||
return QueryResult.invalidScope(proverScopeResult.getFailureMessages()); | ||
} | ||
|
||
// 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 queryspec = convertToQuerySpec(proverScopeResult.getContent()); | ||
var credentialResult = credentialStore.query(queryspec); | ||
if (credentialResult.failed()) { | ||
return QueryResult.storageFailure(credentialResult.getFailureMessages()); | ||
} | ||
|
||
// the credentials requested by the other party | ||
var requestedCredentials = credentialResult.getContent().toList(); | ||
|
||
// check that prover scope is not wider than issuer scope | ||
var issuerQuery = convertToQuerySpec(issuerScopeResult.getContent()); | ||
var predicate = issuerQuery.getFilterExpression().stream() | ||
.map(c -> credentialsPredicate(c.getOperandRight().toString())) | ||
.reduce(Predicate::or) | ||
.orElse(x -> false); | ||
|
||
// now narrow down the requested credentials to only contain allowed credentials | ||
var isValidQuery = requestedCredentials.stream().filter(predicate).count() == requestedCredentials.size(); | ||
|
||
return isValidQuery ? | ||
QueryResult.success(requestedCredentials.stream().map(VerifiableCredentialResource::getVerifiableCredential)) | ||
: QueryResult.unauthorized("Invalid query: requested Credentials outside of scope."); | ||
} | ||
|
||
/** | ||
* Returns a predicate that filters {@link VerifiableCredentialResource} objects based on the provided type by | ||
* inspecting the {@code types} property of the {@link org.eclipse.edc.identitytrust.model.VerifiableCredential} that is | ||
* encapsulated in the resource. | ||
* | ||
* @param type The type to filter by. | ||
* @return A predicate that filters {@link VerifiableCredentialResource} objects based on the provided type. | ||
*/ | ||
private Predicate<VerifiableCredentialResource> credentialsPredicate(String type) { | ||
return resource -> { | ||
var cred = resource.getVerifiableCredential(); | ||
return cred != null && cred.credential() != null && cred.credential().getTypes().contains(type); | ||
}; | ||
} | ||
|
||
/** | ||
* Parses a list of scope strings, converts them to {@link Criterion} objects, and returns a {@link Result} containing | ||
* the list of converted criteria. If any scope string fails to be converted, a failure result is returned. | ||
* | ||
* @param scopes The list of scope strings to parse and convert. | ||
* @return A {@link Result} containing the list of converted {@link Criterion} objects. | ||
*/ | ||
private Result<List<Criterion>> parseScopes(List<String> scopes) { | ||
var transformResult = scopes.stream() | ||
.map(scopeTransformer::transform) | ||
.toList(); | ||
|
||
if (transformResult.stream().anyMatch(AbstractResult::failed)) { | ||
return failure(transformResult.stream().flatMap(r -> r.getFailureMessages().stream()).toList()); | ||
} | ||
|
||
return success(transformResult.stream().map(AbstractResult::getContent).toList()); | ||
} | ||
|
||
private QuerySpec convertToQuerySpec(List<Criterion> criteria) { | ||
return QuerySpec.Builder.newInstance() | ||
.filter(criteria) | ||
.build(); | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
...re/src/main/java/org/eclipse/edc/identityhub/defaults/EdcScopeToCriterionTransformer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/* | ||
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) | ||
* | ||
* This program and the accompanying materials are made available under the | ||
* terms of the Apache License, Version 2.0 which is available at | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* Contributors: | ||
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation | ||
* | ||
*/ | ||
|
||
package org.eclipse.edc.identityhub.defaults; | ||
|
||
import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; | ||
import org.eclipse.edc.spi.query.Criterion; | ||
import org.eclipse.edc.spi.result.Result; | ||
|
||
import java.util.List; | ||
|
||
import static org.eclipse.edc.spi.result.Result.failure; | ||
import static org.eclipse.edc.spi.result.Result.success; | ||
|
||
/** | ||
* Implementation of the {@link ScopeToCriterionTransformer} interface that converts a scope string to a {@link Criterion} object. | ||
* This is a default/example implementation, that assumes scope strings adhere to the following format: | ||
* <pre> | ||
* org.eclipse.edc.vc.type:SomeCredential:[read|all|*] | ||
* </pre> | ||
* This scope string will get translated into a {@link Criterion} like: | ||
* <pre> | ||
* verifiableCredential.credential.types like SomeCredential | ||
* </pre> | ||
* | ||
* <em>This MUST be adapted to the needs and requirements of the dataspace!</em> | ||
* <em>Do NOT use this in production code!</em> | ||
*/ | ||
public class EdcScopeToCriterionTransformer implements ScopeToCriterionTransformer { | ||
public static final String TYPE_OPERAND = "verifiableCredential.credential.types"; | ||
public static final String ALIAS_LITERAL = "org.eclipse.edc.vc.type"; | ||
public static final String LIKE_OPERATOR = "like"; | ||
private static final String SCOPE_SEPARATOR = ":"; | ||
private final List<String> allowedOperations = List.of("read", "*", "all"); | ||
|
||
@Override | ||
public Result<Criterion> transform(String scope) { | ||
var tokens = parseScope(scope); | ||
if (tokens.failed()) { | ||
return failure("Scope string cannot be converted: %s".formatted(tokens.getFailureDetail())); | ||
} | ||
var credentialType = tokens.getContent()[1]; | ||
return success(new Criterion(TYPE_OPERAND, LIKE_OPERATOR, credentialType)); | ||
} | ||
|
||
private Result<String[]> parseScope(String scope) { | ||
if (scope == null) return failure("Scope was null"); | ||
|
||
var tokens = scope.split(SCOPE_SEPARATOR); | ||
if (tokens.length != 3) { | ||
return failure("Scope string has invalid format."); | ||
} | ||
if (!ALIAS_LITERAL.equalsIgnoreCase(tokens[0])) { | ||
return failure("Scope alias MUST be %s but was %s".formatted(ALIAS_LITERAL, tokens[0])); | ||
} | ||
if (!allowedOperations.contains(tokens[2])) { | ||
return failure("Invalid scope operation: " + tokens[2]); | ||
} | ||
|
||
return success(tokens); | ||
} | ||
} |
Oops, something went wrong.