Skip to content

Commit

Permalink
fix: various small fixes and improvements for IATP (#3643)
Browse files Browse the repository at this point in the history
* fix: various small fixes, improvements and changes for IATP

CredentialService url must be resolved dynamically

* criterion-to-predicate-converter supports `contains` operator for collections

* Jws2020 schema: `proof.created` is now optional

* VC/VP: allow null IDs in CredentialSubject, ignore proof objects

* DefaultCredentialServiceClient is provided by the IdentityAndTrustExtension

it is not a default provider anymore, to avoid potential injection race conditions.

* fix some intermittent compile errors

* DEPENDENCIES

* DEPENDENCIES
  • Loading branch information
paullatzelsperger authored Nov 23, 2023
1 parent dda21ff commit 2b5f079
Show file tree
Hide file tree
Showing 21 changed files with 182 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.eclipse.edc.util.reflection.ReflectionUtil;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
Expand All @@ -40,6 +41,7 @@ public <T> Predicate<T> convert(Criterion criterion) {
case "=" -> equalPredicate(criterion);
case "in" -> inPredicate(criterion);
case "like" -> likePredicate(criterion);
case "contains" -> containsPredicate(criterion);
default ->
throw new IllegalArgumentException(format("Operator [%s] is not supported by this converter!", criterion.getOperator()));
};
Expand All @@ -53,6 +55,23 @@ protected Object property(String key, Object object) {
}
}

private <T> Predicate<T> containsPredicate(Criterion criterion) {
return t -> {
var operandLeft = (String) criterion.getOperandLeft();
var operandRight = criterion.getOperandRight();
var property = property(operandLeft, t);
if (property == null) {
return false;
}

if (property instanceof Collection<?> collection) {
return collection.contains(operandRight);
}

return false;
};
}

@NotNull
private <T> Predicate<T> equalPredicate(Criterion criterion) {
return t -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,39 @@ void like_shouldThrowException_whenPropertyDoesNotExits() {
assertThat(predicate).rejects(new TestObject(List.of(new NestedObject("any"))));
}

@Test
void contains_success() {
var predicate = converter.convert(new Criterion("values", "contains", "bar"));
assertThat(predicate).accepts(new StringTestObject(List.of("foo", "bar")));
}

@Test
void contains_typesNotMatch() {
var predicate = converter.convert(new Criterion("values", "contains", 42));
assertThat(predicate).rejects(new StringTestObject(List.of("foo", "bar")));

var predicate2 = converter.convert(new Criterion("values", "contains", "42"));
assertThat(predicate2).accepts(new StringTestObject(List.of("foo", "bar", "42")));
}

@Test
void contains_notCollection() {
var predicate = converter.convert(new Criterion("value", "contains", 42));
assertThat(predicate).rejects(new TestObject("someval"));
}

@Test
void contains_notContained() {
var predicate = converter.convert(new Criterion("values", "contains", "baz"));
assertThat(predicate).rejects(new StringTestObject(List.of("foo", "bar")));
}

@Test
void contains_propertyDoesNotExist() {
var predicate = converter.convert(new Criterion("notexist", "contains", "bar"));
assertThat(predicate).rejects(new StringTestObject(List.of("foo", "bar")));
}

public enum TestEnum {
ENTRY1, ENTRY2
}
Expand Down Expand Up @@ -167,6 +200,10 @@ public String toString() {
}
}

private record NestedObject(String value) { }
private record NestedObject(String value) {
}

private record StringTestObject(List<String> values) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public static LdSchema create(ObjectMapper mapper) {
type(JSON_WEB_SIGNATURE_TYPE).required(),
property(CREATED, xsdDateTime())
.test(created -> Instant.now().isAfter(created))
.required(),
.optional(),
property(CONTROLLER, link()),
property(PURPOSE, link()).required().test(uri -> uri.toString().equals("https://w3id.org/security#assertionMethod")),
verificationMethod(VERIFICATION_METHOD, getVerificationMethod(mapper).map(new JwkAdapter())).required(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,48 +14,37 @@

package org.eclipse.edc.iam.identitytrust.core;

import jakarta.json.Json;
import org.eclipse.edc.iam.identitytrust.core.defaults.DefaultCredentialServiceClient;
import org.eclipse.edc.iam.identitytrust.core.defaults.DefaultTrustedIssuerRegistry;
import org.eclipse.edc.iam.identitytrust.core.defaults.InMemorySignatureSuiteRegistry;
import org.eclipse.edc.iam.identitytrust.core.scope.IatpScopeExtractorRegistry;
import org.eclipse.edc.iam.identitytrust.sts.embedded.EmbeddedSecureTokenService;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
import org.eclipse.edc.identitytrust.AudienceResolver;
import org.eclipse.edc.identitytrust.SecureTokenService;
import org.eclipse.edc.identitytrust.TrustedIssuerRegistry;
import org.eclipse.edc.identitytrust.scope.ScopeExtractorRegistry;
import org.eclipse.edc.identitytrust.verification.SignatureSuiteRegistry;
import org.eclipse.edc.jsonld.spi.JsonLd;
import org.eclipse.edc.jwt.TokenGenerationServiceImpl;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.http.EdcHttpClient;
import org.eclipse.edc.spi.security.KeyPairFactory;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;

import java.security.KeyPair;
import java.time.Clock;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static org.eclipse.edc.spi.CoreConstants.JSON_LD;

@Extension("Identity And Trust Extension to register default services")
public class IatpDefaultServicesExtension implements ServiceExtension {

@Setting(value = "Alias of private key used for signing tokens, retrieved from private key resolver", defaultValue = "A random EC private key")
public static final String STS_PRIVATE_KEY_ALIAS = "edc.iam.sts.privatekey.alias";
@Setting(value = "Alias of public key used for verifying the tokens, retrieved from the vault", defaultValue = "A random EC public key")
public static final String STS_PUBLIC_KEY_ALIAS = "edc.iam.sts.publickey.alias";
@Setting(value = "URL of the CredentialService used to present credentials", required = true)
public static final String CREDENTIALSERVICE_URL_PROPERTY = "edc.iam.credentialservice.url";
// not a setting, it's defined in Oauth2ServiceExtension
private static final String OAUTH_TOKENURL_PROPERTY = "edc.oauth.token.url";
@Setting(value = "Self-issued ID Token expiration in minutes. By default is 5 minutes", defaultValue = "" + IatpDefaultServicesExtension.DEFAULT_STS_TOKEN_EXPIRATION_MIN)
Expand All @@ -68,18 +57,6 @@ public class IatpDefaultServicesExtension implements ServiceExtension {
@Inject
private Clock clock;

@Inject
private TypeTransformerRegistry typeTransformerRegistry;

@Inject
private EdcHttpClient httpClient;

@Inject
private TypeManager typeManager;

@Inject
private JsonLd jsonLd;

@Provider(isDefault = true)
public SecureTokenService createDefaultTokenService(ServiceExtensionContext context) {
context.getMonitor().info("Using the Embedded STS client, as no other implementation was provided.");
Expand Down Expand Up @@ -111,10 +88,8 @@ public ScopeExtractorRegistry scopeExtractorRegistry() {
}

@Provider(isDefault = true)
public CredentialServiceClient createClient(ServiceExtensionContext context) {
return new DefaultCredentialServiceClient(httpClient, Json.createBuilderFactory(Map.of()),
typeManager.getMapper(JSON_LD), typeTransformerRegistry, jsonLd, context.getMonitor(),
context.getConfig().getString(CREDENTIALSERVICE_URL_PROPERTY));
public AudienceResolver identityResolver() {
return identity -> identity;
}

private KeyPair keyPairFromConfig(ServiceExtensionContext context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@

package org.eclipse.edc.iam.identitytrust.core;

import jakarta.json.Json;
import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry;
import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService;
import org.eclipse.edc.iam.identitytrust.core.defaults.DefaultCredentialServiceClient;
import org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator;
import org.eclipse.edc.iam.identitytrust.verification.MultiFormatPresentationVerifier;
import org.eclipse.edc.identitytrust.CredentialServiceClient;
Expand All @@ -30,23 +32,26 @@
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.http.EdcHttpClient;
import org.eclipse.edc.spi.iam.IdentityService;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.verifiablecredentials.jwt.JwtPresentationVerifier;
import org.eclipse.edc.verifiablecredentials.linkeddata.LdpVerifier;
import org.eclipse.edc.verification.jwt.SelfIssuedIdTokenVerifier;

import java.time.Clock;
import java.util.Map;

import static org.eclipse.edc.spi.CoreConstants.JSON_LD;

@Extension("Identity And Trust Extension")
public class IdentityAndTrustExtension implements ServiceExtension {

@Setting(value = "DID of this connector", required = true)
public static final String ISSUER_DID_PROPERTY = "edc.iam.issuer.id";
public static final String CONNECTOR_DID_PROPERTY = "edc.iam.issuer.id";


@Inject
Expand All @@ -73,6 +78,10 @@ public class IdentityAndTrustExtension implements ServiceExtension {

@Inject
private Clock clock;
@Inject
private EdcHttpClient httpClient;
@Inject
private TypeTransformerRegistry typeTransformerRegistry;


private JwtValidator jwtValidator;
Expand All @@ -81,8 +90,8 @@ public class IdentityAndTrustExtension implements ServiceExtension {

@Provider
public IdentityService createIdentityService(ServiceExtensionContext context) {
return new IdentityAndTrustService(secureTokenService, getIssuerDid(context), context.getParticipantId(), getPresentationVerifier(context),
credentialServiceClient, getJwtValidator(), getJwtVerifier(), registry, clock);
return new IdentityAndTrustService(secureTokenService, getOwnDid(context), context.getParticipantId(), getPresentationVerifier(context),
getCredentialServiceclient(context), getJwtValidator(), getJwtVerifier(), registry, clock);
}

@Provider
Expand All @@ -93,6 +102,15 @@ public JwtValidator getJwtValidator() {
return jwtValidator;
}

@Provider
public CredentialServiceClient getCredentialServiceclient(ServiceExtensionContext context) {
if (credentialServiceClient == null) {
credentialServiceClient = new DefaultCredentialServiceClient(httpClient, Json.createBuilderFactory(Map.of()),
typeManager.getMapper(JSON_LD), typeTransformerRegistry, jsonLd, context.getMonitor());
}
return credentialServiceClient;
}

@Provider
public PresentationVerifier getPresentationVerifier(ServiceExtensionContext context) {
if (presentationVerifier == null) {
Expand Down Expand Up @@ -120,11 +138,6 @@ public JwtVerifier getJwtVerifier() {


private String getOwnDid(ServiceExtensionContext context) {
// todo: this must be config value
return null;
}

private String getIssuerDid(ServiceExtensionContext context) {
return context.getConfig().getString(ISSUER_DID_PROPERTY);
return context.getConfig().getString(CONNECTOR_DID_PROPERTY);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,18 @@ public class DefaultCredentialServiceClient implements CredentialServiceClient {
private final TypeTransformerRegistry transformerRegistry;
private final JsonLd jsonLd;
private final Monitor monitor;
private final String credentialServiceUrl;

public DefaultCredentialServiceClient(EdcHttpClient httpClient, JsonBuilderFactory jsonFactory, ObjectMapper jsonLdMapper, TypeTransformerRegistry transformerRegistry, JsonLd jsonLd, Monitor monitor, String credentialServiceUrl) {
public DefaultCredentialServiceClient(EdcHttpClient httpClient, JsonBuilderFactory jsonFactory, ObjectMapper jsonLdMapper, TypeTransformerRegistry transformerRegistry, JsonLd jsonLd, Monitor monitor) {
this.httpClient = httpClient;
this.jsonFactory = jsonFactory;
this.objectMapper = jsonLdMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
this.transformerRegistry = transformerRegistry;
this.jsonLd = jsonLd;
this.monitor = monitor;
this.credentialServiceUrl = credentialServiceUrl;
}

@Override
public Result<List<VerifiablePresentationContainer>> requestPresentation(String selfIssuedTokenJwt, List<String> scopes) {
public Result<List<VerifiablePresentationContainer>> requestPresentation(String credentialServiceUrl, String selfIssuedTokenJwt, List<String> scopes) {
var query = createPresentationQuery(scopes);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.spi.CoreConstants.JSON_LD;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
Expand All @@ -49,12 +50,12 @@ void setUp(ServiceExtensionContext context) {
@Test
void verifyCorrectService(IdentityAndTrustExtension extension) {
var configMock = mock(Config.class);
when(configMock.getString(eq(IdentityAndTrustExtension.ISSUER_DID_PROPERTY))).thenReturn("did:web:test");
when(configMock.getString(eq(IdentityAndTrustExtension.CONNECTOR_DID_PROPERTY))).thenReturn("did:web:test");
when(spiedContext.getConfig()).thenReturn(configMock);

var is = extension.createIdentityService(spiedContext);

assertThat(is).isInstanceOf(IdentityAndTrustService.class);
verify(configMock).getString(eq(IdentityAndTrustExtension.ISSUER_DID_PROPERTY));
verify(configMock, atLeastOnce()).getString(eq(IdentityAndTrustExtension.CONNECTOR_DID_PROPERTY));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ void setup() {
var jsonLdMock = mock(JsonLd.class);
when(jsonLdMock.expand(any())).thenAnswer(a -> success(a.getArgument(0)));
client = new DefaultCredentialServiceClient(httpClientMock, Json.createBuilderFactory(Map.of()),
createObjectMapper(), registry, jsonLdMock, mock(), CS_URL);
createObjectMapper(), registry, jsonLdMock, mock());
}

@Test
Expand All @@ -70,7 +70,7 @@ void requestPresentation_singleLdpVp() throws IOException {
when(httpClientMock.execute(any()))
.thenReturn(response(200, getResourceFileContentAsString("single_ldp-vp.json")));

var result = client.requestPresentation("foo", List.of());
var result = client.requestPresentation(CS_URL, "foo", List.of());
assertThat(result.succeeded()).isTrue();
assertThat(result.getContent()).hasSize(1).allMatch(vpc -> vpc.format() == CredentialFormat.JSON_LD);
}
Expand All @@ -81,7 +81,7 @@ void requestPresentation_singleJwtVp() throws IOException {
when(httpClientMock.execute(any()))
.thenReturn(response(200, getResourceFileContentAsString("single_jwt-vp.json")));

var result = client.requestPresentation("foo", List.of());
var result = client.requestPresentation(CS_URL, "foo", List.of());
assertThat(result.succeeded()).isTrue();
assertThat(result.getContent()).hasSize(1).allMatch(vpc -> vpc.format() == CredentialFormat.JWT);
}
Expand All @@ -92,7 +92,7 @@ void requestPresentationLdp_multipleVp_mixed() throws IOException {
when(httpClientMock.execute(any()))
.thenReturn(response(200, getResourceFileContentAsString("multiple_vp-token_mixed.json")));

var result = client.requestPresentation("foo", List.of());
var result = client.requestPresentation(CS_URL, "foo", List.of());
assertThat(result.succeeded()).isTrue();
assertThat(result.getContent()).hasSize(2)
.anySatisfy(vp -> assertThat(vp.format()).isEqualTo(CredentialFormat.JSON_LD))
Expand All @@ -105,7 +105,7 @@ void requestPresentation_mulipleVp_onlyLdp() throws IOException {
when(httpClientMock.execute(any()))
.thenReturn(response(200, getResourceFileContentAsString("multiple_vp-token_ldp.json")));

var result = client.requestPresentation("foo", List.of());
var result = client.requestPresentation(CS_URL, "foo", List.of());
assertThat(result.succeeded()).isTrue();
assertThat(result.getContent()).hasSize(2)
.allSatisfy(vp -> assertThat(vp.format()).isEqualTo(CredentialFormat.JSON_LD));
Expand All @@ -117,7 +117,7 @@ void requestPresentation_mulipleVp_onlyJwt() throws IOException {
when(httpClientMock.execute(any()))
.thenReturn(response(200, getResourceFileContentAsString("multiple_vp-token_jwt.json")));

var result = client.requestPresentation("foo", List.of());
var result = client.requestPresentation(CS_URL, "foo", List.of());
assertThat(result.succeeded()).isTrue();
assertThat(result.getContent()).hasSize(2)
.allSatisfy(vp -> assertThat(vp.format()).isEqualTo(CredentialFormat.JWT));
Expand All @@ -129,11 +129,22 @@ void requestPresentation_csReturnsError(int httpCode) throws IOException {
when(httpClientMock.execute(any()))
.thenReturn(response(httpCode, "Test failure"));

var res = client.requestPresentation("foo", List.of());
var res = client.requestPresentation(CS_URL, "foo", List.of());
assertThat(res.failed()).isTrue();
assertThat(res.getFailureDetail()).isEqualTo("Presentation Query failed: HTTP %s, message: Test failure".formatted(httpCode));
}

@DisplayName("CS returns an empty array, because no VC was found")
@Test
void requestPresentation_emptyArray() throws IOException {
when(httpClientMock.execute(any()))
.thenReturn(response(200, "{\"vp_token\":[],\"presentation_submission\":null}"));

var res = client.requestPresentation(CS_URL, "foo", List.of());
assertThat(res.succeeded()).isTrue();
assertThat(res.getContent()).isNotNull().doesNotContainNull().isEmpty();
}

private VerifiablePresentation createPresentation() {
return VerifiablePresentation.Builder.newInstance()
.type("VerifiablePresentation")
Expand Down
Loading

0 comments on commit 2b5f079

Please sign in to comment.