Skip to content

Commit

Permalink
feat: enable defining supported VC types for trusted issuers (#4454)
Browse files Browse the repository at this point in the history
  • Loading branch information
bscholtes1A authored Sep 16, 2024
1 parent 668d40e commit d8552b2
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,28 +17,26 @@
import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer;
import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
* Simple, memory-based implementation of a {@link TrustedIssuerRegistry}
* Simple, memory-based implementation of a {@link TrustedIssuerRegistry}.
*/
public class DefaultTrustedIssuerRegistry implements TrustedIssuerRegistry {
private final Map<String, Issuer> store = new HashMap<>();

@Override
public void addIssuer(Issuer issuer) {
store.put(issuer.id(), issuer);
}
private final Map<String, Set<String>> store = new ConcurrentHashMap<>();

@Override
public Issuer getById(String id) {
return store.get(id);
public void register(Issuer issuer, String credentialType) {
store.computeIfAbsent(issuer.id(), k -> new HashSet<>()).add(credentialType);
}

@Override
public Collection<Issuer> getTrustedIssuers() {
return store.values();
public Set<String> getSupportedTypes(Issuer issuer) {
return store.getOrDefault(issuer.id(), Set.of());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,40 +26,20 @@ class DefaultTrustedIssuerRegistryTest {
private final DefaultTrustedIssuerRegistry registry = new DefaultTrustedIssuerRegistry();

@Test
void addIssuer() {
void trustedIssuer() {
var issuer = new Issuer("test-id", Map.of());
registry.addIssuer(issuer);
assertThat(registry.getTrustedIssuers()).containsExactly(issuer);
}

@Test
void addIssuer_exists_shouldReplace() {
var issuer = new Issuer("test-id", Map.of());
var issuer2 = new Issuer("test-id", Map.of("new-key", "new-val"));
registry.addIssuer(issuer);
registry.addIssuer(issuer2);
assertThat(registry.getTrustedIssuers()).containsExactly(issuer2);
}

@Test
void getById() {
var issuer = new Issuer("test-id", Map.of());
registry.addIssuer(issuer);
assertThat(registry.getById("test-id")).isEqualTo(issuer);
}
registry.register(issuer, "test-type1");
registry.register(issuer, "test-type2");

@Test
void getById_notFound() {
assertThat(registry.getById("nonexistent-id")).isNull();
assertThat(registry.getSupportedTypes(issuer)).containsExactly("test-type1", "test-type2");
}

@Test
void getTrustedIssuers() {
void invalidIssuer() {
var issuer = new Issuer("test-id", Map.of());
var issuer2 = new Issuer("test-id2", Map.of("new-key", "new-val"));
registry.addIssuer(issuer);
registry.addIssuer(issuer2);

assertThat(registry.getTrustedIssuers()).containsExactlyInAnyOrder(issuer2, issuer);
assertThat(registry.getSupportedTypes(issuer)).isEmpty();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.eclipse.edc.spi.system.configuration.Config;
import org.eclipse.edc.spi.types.TypeManager;

import java.util.List;
import java.util.Map;

import static org.eclipse.edc.iam.identitytrust.issuer.configuration.TrustedIssuerConfigurationExtension.NAME;
Expand All @@ -39,10 +40,12 @@ public class TrustedIssuerConfigurationExtension implements ServiceExtension {
public static final String CONFIG_PREFIX = "edc.iam.trusted-issuer";
public static final String CONFIG_ALIAS = CONFIG_PREFIX + ".<issuerAlias>.";

@Setting(context = CONFIG_ALIAS, value = "Additional properties of the issuer.")
public static final String PROPERTIES_SUFFIX = "properties";
@Setting(context = CONFIG_ALIAS, value = "ID of the issuer.", required = true)
public static final String ID_SUFFIX = "id";
@Setting(context = CONFIG_ALIAS, value = "Additional properties of the issuer.")
public static final String PROPERTIES_SUFFIX = "properties";
@Setting(context = CONFIG_ALIAS, value = "List of supported credential types for this issuer.")
public static final String SUPPORTEDTYPES_SUFFIX = "supportedtypes";

protected static final String NAME = "Trusted Issuers Configuration Extensions";

Expand All @@ -56,19 +59,23 @@ public class TrustedIssuerConfigurationExtension implements ServiceExtension {
@Override
public void initialize(ServiceExtensionContext context) {
var config = context.getConfig(CONFIG_PREFIX);
var issuers = config.partition().map(this::configureIssuer).toList();
if (issuers.isEmpty()) {
var configs = config.partition().toList();
if (configs.isEmpty()) {
monitor.warning("The list of trusted issuers is empty");
}
issuers.forEach(issuer -> trustedIssuerRegistry.addIssuer(issuer));
}

private Issuer configureIssuer(Config config) {
configs.forEach(this::addIssuer);
}

private void addIssuer(Config config) {
var id = config.getString(ID_SUFFIX);
var supportedTypesConfig = config.getString(SUPPORTEDTYPES_SUFFIX, "[\"%s\"]".formatted(TrustedIssuerRegistry.WILDCARD));
var supportedTypes = typeManager.readValue(supportedTypesConfig, new TypeReference<List<String>>() {
});
var propertiesConfig = config.getString(PROPERTIES_SUFFIX, "{}");
var properties = typeManager.readValue(propertiesConfig, new TypeReference<Map<String, Object>>() {
});
return new Issuer(id, properties);

supportedTypes.forEach(type -> trustedIssuerRegistry.register(new Issuer(id, properties), type));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
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.times;
import static org.mockito.Mockito.verify;
Expand All @@ -49,13 +51,24 @@ void setup(ServiceExtensionContext context) {
}

@Test
void initialize(ServiceExtensionContext context, TrustedIssuerConfigurationExtension ext) {
var cfg = ConfigFactory.fromMap(Map.of("issuer1.id", "issuerId1"));
void initialize_issuerWithSupportedTypes(ServiceExtensionContext context, TrustedIssuerConfigurationExtension ext) {
var cfg = ConfigFactory.fromMap(Map.of("issuer1.id", "issuer1", "issuer1.supportedtypes", "[\"type1\", \"type2\"]"));
when(context.getConfig("edc.iam.trusted-issuer")).thenReturn(cfg);

ext.initialize(context);

verify(trustedIssuerRegistry).addIssuer(argThat(issuer -> issuer.id().equals("issuerId1")));
verify(trustedIssuerRegistry).register(argThat(issuer -> issuer.id().equals("issuer1")), eq("type1"));
verify(trustedIssuerRegistry).register(argThat(issuer -> issuer.id().equals("issuer1")), eq("type1"));
}

@Test
void initialize_issuerWithoutSupportedType(ServiceExtensionContext context, TrustedIssuerConfigurationExtension ext) {
var cfg = ConfigFactory.fromMap(Map.of("issuer1.id", "issuer1"));
when(context.getConfig("edc.iam.trusted-issuer")).thenReturn(cfg);

ext.initialize(context);

verify(trustedIssuerRegistry).register(argThat(issuer -> issuer.id().equals("issuer1")), eq(TrustedIssuerRegistry.WILDCARD));
}

@Test
Expand All @@ -79,7 +92,7 @@ void initialize_withProperties(ServiceExtensionContext context, TrustedIssuerCon

ext.initialize(context);

verify(trustedIssuerRegistry).addIssuer(argThat(issuer -> issuer.additionalProperties().get("custom").equals("test")));
verify(trustedIssuerRegistry).register(argThat(issuer -> issuer.additionalProperties().get("custom").equals("test")), eq(TrustedIssuerRegistry.WILDCARD));
}

@Test
Expand All @@ -91,7 +104,7 @@ void initialize_withTwoIssuers(ServiceExtensionContext context, TrustedIssuerCon

var issuers = ArgumentCaptor.forClass(Issuer.class);

verify(trustedIssuerRegistry, times(2)).addIssuer(issuers.capture());
verify(trustedIssuerRegistry, times(2)).register(issuers.capture(), any());

assertThat(issuers.getAllValues()).hasSize(2)
.extracting(Issuer::id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import org.eclipse.edc.iam.verifiablecredentials.rules.IsInValidityPeriod;
import org.eclipse.edc.iam.verifiablecredentials.rules.IsNotRevoked;
import org.eclipse.edc.iam.verifiablecredentials.spi.VerifiableCredentialValidationService;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.RevocationServiceRegistry;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiablePresentationContainer;
Expand Down Expand Up @@ -68,7 +67,7 @@ private Result<Void> validateVerifiableCredentials(List<VerifiableCredential> cr
new IsInValidityPeriod(clock),
new HasValidSubjectIds(presentationHolder),
new IsNotRevoked(revocationServiceRegistry),
new HasValidIssuer(getTrustedIssuerIds())));
new HasValidIssuer(trustedIssuerRegistry)));

filters.addAll(additionalRules);
var results = credentials
Expand All @@ -78,7 +77,4 @@ private Result<Void> validateVerifiableCredentials(List<VerifiableCredential> cr
return results.orElseGet(() -> failure("Could not determine the status of the VC validation"));
}

private List<String> getTrustedIssuerIds() {
return trustedIssuerRegistry.getTrustedIssuers().stream().map(Issuer::id).toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,23 @@

import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential;
import org.eclipse.edc.iam.verifiablecredentials.spi.validation.CredentialValidationRule;
import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry;
import org.eclipse.edc.spi.result.Result;

import java.util.Collection;
import java.util.Collections;

import static org.eclipse.edc.spi.result.Result.failure;
import static org.eclipse.edc.spi.result.Result.success;

/**
* A class that implements the {@link CredentialValidationRule} interface and checks if a {@link VerifiableCredential} has a valid issuer.
* Valid issuers are stored in a global list.
* <p>
* If the issuer object is neither a string nor an object containing an "id" field, a failure is returned.
* A class that implements the {@link CredentialValidationRule} interface and checks if a {@link VerifiableCredential} has a trusted issuer,
* and if the credential type is supported for this issuer.
*/
public class HasValidIssuer implements CredentialValidationRule {
private final Collection<String> trustedIssuers;
private final TrustedIssuerRegistry trustedIssuerRegistry;

public HasValidIssuer(Collection<String> trustedIssuers) {
this.trustedIssuers = trustedIssuers;
public HasValidIssuer(TrustedIssuerRegistry trustedIssuerRegistry) {
this.trustedIssuerRegistry = trustedIssuerRegistry;
}

@Override
Expand All @@ -42,6 +41,11 @@ public Result<Void> apply(VerifiableCredential credential) {
if (issuer.id() == null) {
return failure("Issuer did not contain an 'id' field.");
}
return trustedIssuers.contains(issuer.id()) ? success() : failure("Issuer '%s' is not in the list of trusted issuers".formatted(issuer.id()));

var supportedTypes = trustedIssuerRegistry.getSupportedTypes(issuer);
return !supportedTypes.contains(TrustedIssuerRegistry.WILDCARD) && Collections.disjoint(credential.getType(), supportedTypes) ?
failure("Credential types '%s' are not supported for issuer '%s'".formatted(credential.getType(), issuer.id())) :
success();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class VerifiableCredentialValidationServiceImplTest {

@BeforeEach
void setUp() {
when(trustedIssuerRegistryMock.getSupportedTypes(TRUSTED_ISSUER)).thenReturn(Set.of(TrustedIssuerRegistry.WILDCARD));
when(revocationServiceRegistry.checkValidity(any())).thenReturn(Result.success());
}

Expand Down Expand Up @@ -103,7 +104,6 @@ void oneInvalidSubjectId() {

@Test
void credentialHasInvalidIssuer_issuerIsUrl() {
var consumerDid = "did:web:test-consumer";
var presentation = createPresentationBuilder()
.type("VerifiablePresentation")
.credentials(List.of(createCredentialBuilder()
Expand All @@ -116,7 +116,7 @@ void credentialHasInvalidIssuer_issuerIsUrl() {
var result = service.validate(List.of(vpContainer));
assertThat(result).isFailed().messages()
.hasSizeGreaterThanOrEqualTo(1)
.contains("Issuer 'invalid-issuer' is not in the list of trusted issuers");
.contains("Credential types '[test-type]' are not supported for issuer 'invalid-issuer'");
}

@Test
Expand All @@ -133,7 +133,6 @@ void verify_singlePresentation_singleCredential() {
.build();
var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation);
when(verifierMock.verifyPresentation(any())).thenReturn(success());
when(trustedIssuerRegistryMock.getTrustedIssuers()).thenReturn(Set.of(TRUSTED_ISSUER));
var result = service.validate(List.of(vpContainer));
assertThat(result).isSucceeded();
}
Expand All @@ -158,7 +157,6 @@ void verify_singlePresentation_multipleCredentials() {
.build();
var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation);
when(verifierMock.verifyPresentation(any())).thenReturn(success());
when(trustedIssuerRegistryMock.getTrustedIssuers()).thenReturn(Set.of(TRUSTED_ISSUER));
var result = service.validate(List.of(vpContainer));
assertThat(result).isSucceeded();
}
Expand Down Expand Up @@ -202,8 +200,6 @@ void verify_multiplePresentations_multipleCredentialsEach() {
var vpContainer2 = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation2);

when(verifierMock.verifyPresentation(any())).thenReturn(success());
when(trustedIssuerRegistryMock.getTrustedIssuers()).thenReturn(Set.of(TRUSTED_ISSUER));


var result = service.validate(List.of(vpContainer1, vpContainer2));
assertThat(result).isSucceeded();
Expand All @@ -224,7 +220,6 @@ void verify_revocationCheckFails() {
.build();
var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation);
when(verifierMock.verifyPresentation(any())).thenReturn(success());
when(trustedIssuerRegistryMock.getTrustedIssuers()).thenReturn(Set.of(TRUSTED_ISSUER));
when(revocationServiceRegistry.checkValidity(any())).thenReturn(Result.failure("invalid"));

var result = service.validate(List.of(vpContainer));
Expand Down
Loading

0 comments on commit d8552b2

Please sign in to comment.