Skip to content

Commit 1235a96

Browse files
feat: api for discriminator aliases (#956)
* feat: add API to register discriminator mappings * e2e test * cleanup * api version file
1 parent a125404 commit 1235a96

10 files changed

Lines changed: 174 additions & 12 deletions

File tree

core/common-core/src/main/java/org/eclipse/edc/identityhub/defaults/DiscriminatorMappingRegistryImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public class DiscriminatorMappingRegistryImpl implements DiscriminatorMappingReg
3333

3434
@Override
3535
public void addMapping(String alias, String discriminator) {
36+
37+
if (mappings.containsValue(discriminator)) {
38+
throw new IllegalArgumentException("An alias for this discriminator already exists: '%s' -> '%s'".formatted(alias, discriminator));
39+
}
40+
3641
mappings.put(alias, discriminator);
3742
}
3843

core/common-core/src/test/java/org/eclipse/edc/identityhub/defaults/DiscriminatorMappingRegistryImplTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.junit.jupiter.api.Test;
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2021

2122
class DiscriminatorMappingRegistryImplTest {
2223

@@ -60,4 +61,10 @@ void addMapping_withMultipleMappings_shouldStoreAll() {
6061
assertThat(registry.getMapping("alias3")).isEqualTo("discriminator3");
6162
}
6263

64+
@Test
65+
void addMapping_duplicateDiscriminator_shouldThrowException() {
66+
registry.addMapping("alias1", "discriminator1");
67+
assertThatThrownBy(() -> registry.addMapping("alias2", "discriminator1"))
68+
.isInstanceOf(IllegalArgumentException.class);
69+
}
6370
}

e2e-tests/identity-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiEndToEndTest.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyDescriptor;
4141
import org.eclipse.edc.identityhub.spi.participantcontext.model.KeyPairUsage;
4242
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantManifest;
43+
import org.eclipse.edc.identityhub.spi.transformation.DiscriminatorMappingRegistry;
4344
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus;
4445
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource;
4546
import org.eclipse.edc.identityhub.spi.verifiablecredentials.store.CredentialStore;
@@ -747,6 +748,49 @@ void query_testScopesWithSingleCredential(String scope, IdentityHub identityHub,
747748
}));
748749
}
749750

751+
@Test
752+
void query_withDiscriminatorAlias(IdentityHub identityHub, CredentialStore store, DiscriminatorMappingRegistry mappingRegistry) throws JsonProcessingException, JOSEException {
753+
// both these credentials have the same type, but difference namespaces/contexts
754+
var credResource = storeCredential(TestData.VC_EXAMPLE_OTHER_NAMESPACE, CredentialFormat.VC1_0_JWT, store);
755+
756+
// providing no specific scope should cause all credentials to be returned -> clash
757+
758+
when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(CONSUMER_KEY.toPublicKey()));
759+
when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(PROVIDER_KEY.toPublicKey()));
760+
761+
var credential = credResource.getVerifiableCredential().credential();
762+
var context = credential.getContext().get(0);
763+
var type = credential.getType().get(0);
764+
var fqct = context + "#" + type;
765+
var alias = "MyCred";
766+
767+
mappingRegistry.addMapping(alias, fqct);
768+
var token = generateSiToken("org.eclipse.dspace.dcp.vc.type:%s:read".formatted(alias));
769+
770+
771+
var response = identityHub.getCredentialsEndpoint().baseRequest()
772+
.contentType(JSON)
773+
.header(AUTHORIZATION, "Bearer " + token)
774+
.body(VALID_QUERY_WITH_FQCT_SCOPE_TEMPLATE.formatted(DSPACE_DCP_V_1_0_CONTEXT, "org.eclipse.dspace.dcp.vc.type:%s:read".formatted(alias)))
775+
.post("/v1/participants/%s/presentations/query".formatted(TEST_PARTICIPANT_CONTEXT_ID))
776+
.then()
777+
.statusCode(200)
778+
.log().ifValidationFails()
779+
.extract().body().as(JsonObject.class);
780+
781+
assertThat(response)
782+
.hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage"))
783+
.hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(1))
784+
.hasEntrySatisfying("presentation", jsonValue ->
785+
assertThat(vpTokensExtractor(jsonValue)).hasSize(1)
786+
.first()
787+
.satisfies(vpToken -> {
788+
assertThat(vpToken).isNotNull();
789+
var credentials = extractCredentials(vpToken);
790+
assertThat(credentials).hasSize(1);
791+
}));
792+
}
793+
750794
/**
751795
* extracts a (potentially empty) list of verifiable credentials from a JWT-VP
752796
*/

e2e-tests/identity-api-tests/src/test/java/org/eclipse/edc/identityhub/tests/fixtures/TestFunctions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public static VerifiableCredential createCredential() {
8787
.type("test-type")
8888
.issuanceDate(Instant.now())
8989
.issuer(new Issuer("did:web:issuer"))
90+
.context("https://example.com/contexts/v1")
9091
.credentialSubject(CredentialSubject.Builder.newInstance().id("id").claim("foo", "bar").build())
9192
.build();
9293
}

extensions/api/identity-api/identity-api-configuration/src/main/resources/identity-api-version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"version": "1.0.0-alpha",
44
"urlPath": "/v1alpha",
5-
"lastUpdated": "2026-02-17T16:00:00Z",
5+
"lastUpdated": "2026-03-25T11:00:00Z",
66
"maturity": null
77
}
88
]

extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/VerifiableCredentialApiExtension.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.GetAllCredentialsApiController;
2020
import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.VerifiableCredentialsApiController;
2121
import org.eclipse.edc.identityhub.api.verifiablecredentials.v1.unstable.transformer.VerifiableCredentialManifestToVerifiableCredentialResourceTransformer;
22+
import org.eclipse.edc.identityhub.spi.transformation.DiscriminatorMappingRegistry;
2223
import org.eclipse.edc.identityhub.spi.verifiablecredentials.CredentialRequestManager;
2324
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource;
2425
import org.eclipse.edc.identityhub.spi.verifiablecredentials.store.CredentialStore;
@@ -49,6 +50,8 @@ public class VerifiableCredentialApiExtension implements ServiceExtension {
4950
private AuthorizationService authorizationService;
5051
@Inject
5152
private CredentialRequestManager credentialRequestManager;
53+
@Inject
54+
private DiscriminatorMappingRegistry discriminatorMappingRegistry;
5255

5356
@Override
5457
public String name() {
@@ -60,7 +63,7 @@ public void initialize(ServiceExtensionContext context) {
6063
authorizationService.addLookupFunction(VerifiableCredentialResource.class, this::queryById);
6164
var registry = typeTransformerRegistry.forContext("identity-api");
6265
registry.register(new VerifiableCredentialManifestToVerifiableCredentialResourceTransformer());
63-
var controller = new VerifiableCredentialsApiController(credentialStore, authorizationService, new VerifiableCredentialManifestValidator(), registry, credentialRequestManager);
66+
var controller = new VerifiableCredentialsApiController(credentialStore, authorizationService, new VerifiableCredentialManifestValidator(), registry, credentialRequestManager, discriminatorMappingRegistry);
6467
var getAllController = new GetAllCredentialsApiController(credentialStore);
6568
webService.registerResource(IdentityHubApiContext.IDENTITY, controller);
6669
webService.registerResource(IdentityHubApiContext.IDENTITY, getAllController);

extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApi.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.eclipse.edc.web.spi.ApiErrorDetail;
3434

3535
import java.util.Collection;
36+
import java.util.Map;
3637

3738
@OpenAPIDefinition(info = @Info(description = "This is the Identity API for manipulating VerifiableCredentials", title = "VerifiableCredentials Identity API", version = "1"))
3839
@Tag(name = "Verifiable Credentials")
@@ -104,7 +105,7 @@ public interface VerifiableCredentialsApi {
104105
@Operation(description = "Delete a VerifiableCredential.",
105106
operationId = "deleteCredential",
106107
responses = {
107-
@ApiResponse(responseCode = "200", description = "The VerifiableCredential was deleted successfully", content = {@Content(schema = @Schema(implementation = String.class))}),
108+
@ApiResponse(responseCode = "200", description = "The VerifiableCredential was deleted successfully", content = { @Content(schema = @Schema(implementation = String.class)) }),
108109
@ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed",
109110
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")),
110111
@ApiResponse(responseCode = "403", description = "The request could not be completed, because either the authentication was missing or was not valid.",
@@ -144,4 +145,14 @@ public interface VerifiableCredentialsApi {
144145
}
145146
)
146147
HolderCredentialRequestDto getCredentialRequest(String participantContextId, String holderPid, SecurityContext securityContext);
148+
149+
@Operation(description = "Define a scope discriminator mapping (=short-hand) for the fully-qualified credential type",
150+
operationId = "addDiscriminatorMapping",
151+
requestBody = @RequestBody(description = "Mapping between alias (short-hand) and discriminator (fully-qualified credential type)", required = true),
152+
responses = {
153+
@ApiResponse(responseCode = "204", description = "Discriminator mapping added successfully"),
154+
@ApiResponse(responseCode = "400", description = "Request body was malformed, or the request could not be processed",
155+
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetail.class)), mediaType = "application/json")),
156+
})
157+
void addDiscriminatorMapping(String participantContextId, Map<String, String> mappings, SecurityContext securityContext);
147158
}

extensions/api/identity-api/verifiable-credentials-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredentials/v1/unstable/VerifiableCredentialsApiController.java

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.eclipse.edc.identityhub.spi.credential.request.model.HolderCredentialRequest;
3939
import org.eclipse.edc.identityhub.spi.credential.request.model.RequestedCredential;
4040
import org.eclipse.edc.identityhub.spi.participantcontext.model.IdentityHubParticipantContext;
41+
import org.eclipse.edc.identityhub.spi.transformation.DiscriminatorMappingRegistry;
4142
import org.eclipse.edc.identityhub.spi.verifiablecredentials.CredentialRequestManager;
4243
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialManifest;
4344
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource;
@@ -54,6 +55,7 @@
5455

5556
import java.net.URI;
5657
import java.util.Collection;
58+
import java.util.Map;
5759
import java.util.UUID;
5860

5961
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
@@ -71,23 +73,26 @@ public class VerifiableCredentialsApiController implements VerifiableCredentials
7173
private final VerifiableCredentialManifestValidator validator;
7274
private final TypeTransformerRegistry typeTransformerRegistry;
7375
private final CredentialRequestManager credentialRequestService;
76+
private final DiscriminatorMappingRegistry discriminatorMappingRegistry;
7477

7578
public VerifiableCredentialsApiController(CredentialStore credentialStore,
7679
AuthorizationService authorizationService,
7780
VerifiableCredentialManifestValidator validator,
7881
TypeTransformerRegistry typeTransformerRegistry,
79-
CredentialRequestManager credentialRequestService) {
82+
CredentialRequestManager credentialRequestService,
83+
DiscriminatorMappingRegistry discriminatorMappingRegistry) {
8084
this.credentialStore = credentialStore;
8185
this.authorizationService = authorizationService;
8286
this.validator = validator;
8387
this.typeTransformerRegistry = typeTransformerRegistry;
8488
this.credentialRequestService = credentialRequestService;
89+
this.discriminatorMappingRegistry = discriminatorMappingRegistry;
8590
}
8691

8792
@GET
8893
@Path("/{credentialId}")
8994
@RequiredScope("identity-api:read")
90-
@RolesAllowed({ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT})
95+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
9196
@Override
9297
public VerifiableCredentialResource getCredential(@PathParam("participantContextId") String participantContextId, @PathParam("credentialId") String id, @Context SecurityContext securityContext) {
9398
authorizationService.authorize(securityContext, participantContextId, id, VerifiableCredentialResource.class)
@@ -100,7 +105,7 @@ public VerifiableCredentialResource getCredential(@PathParam("participantContext
100105

101106
@POST
102107
@RequiredScope("identity-api:write")
103-
@RolesAllowed({ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT})
108+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
104109
@Override
105110
public void addCredential(@PathParam("participantContextId") String participantContextId, VerifiableCredentialManifest manifest, @Context SecurityContext securityContext) {
106111
validator.validate(manifest).orElseThrow(ValidationFailureException::new);
@@ -115,7 +120,7 @@ public void addCredential(@PathParam("participantContextId") String participantC
115120

116121
@PUT
117122
@RequiredScope("identity-api:write")
118-
@RolesAllowed({ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT})
123+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
119124
@Override
120125
public void updateCredential(@PathParam("participantContextId") String participantContextId, VerifiableCredentialManifest manifest, @Context SecurityContext securityContext) {
121126
validator.validate(manifest).orElseThrow(ValidationFailureException::new);
@@ -130,7 +135,7 @@ public void updateCredential(@PathParam("participantContextId") String participa
130135

131136
@GET
132137
@RequiredScope("identity-api:read")
133-
@RolesAllowed({ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT})
138+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
134139
@Override
135140
public Collection<VerifiableCredentialResource> queryCredentialsByType(@PathParam("participantContextId") String participantContextId, @Nullable @QueryParam("type") String type, @Context SecurityContext securityContext) {
136141
var query = QuerySpec.Builder.newInstance();
@@ -149,7 +154,7 @@ public Collection<VerifiableCredentialResource> queryCredentialsByType(@PathPara
149154
@DELETE
150155
@Path("/{credentialId}")
151156
@RequiredScope("identity-api:write")
152-
@RolesAllowed({ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT})
157+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
153158
@Override
154159
public void deleteCredential(@PathParam("participantContextId") String participantContextId, @PathParam("credentialId") String id, @Context SecurityContext securityContext) {
155160
authorizationService.authorize(securityContext, participantContextId, id, VerifiableCredentialResource.class)
@@ -163,7 +168,7 @@ public void deleteCredential(@PathParam("participantContextId") String participa
163168
@POST
164169
@Path("/request")
165170
@RequiredScope("identity-api:write")
166-
@RolesAllowed({ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT})
171+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
167172
@Override
168173
public Response requestCredential(@PathParam("participantContextId") String participantContextId, CredentialRequestDto credentialRequestDto, @Context SecurityContext securityContext) {
169174
authorizationService.authorize(securityContext, participantContextId, participantContextId, IdentityHubParticipantContext.class)
@@ -180,7 +185,7 @@ public Response requestCredential(@PathParam("participantContextId") String part
180185
@GET
181186
@Path("/request/{holderPid}")
182187
@RequiredScope("identity-api:read")
183-
@RolesAllowed({ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT})
188+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
184189
@Override
185190
public HolderCredentialRequestDto getCredentialRequest(@PathParam("participantContextId") String participantContextId,
186191
@PathParam("holderPid") String holderPid,
@@ -194,4 +199,19 @@ public HolderCredentialRequestDto getCredentialRequest(@PathParam("participantCo
194199
.orElseThrow(() -> new ObjectNotFoundException(HolderCredentialRequest.class, holderPid));
195200
}
196201

202+
@POST
203+
@Path("/discriminator")
204+
@RequiredScope("identity-api:write")
205+
@RolesAllowed({ ParticipantPrincipal.ROLE_ADMIN, ParticipantPrincipal.ROLE_PARTICIPANT })
206+
@Override
207+
public void addDiscriminatorMapping(@PathParam("participantContextId") String participantContextId,
208+
Map<String, String> discriminatorMappings,
209+
@Context SecurityContext securityContext) {
210+
211+
authorizationService.authorize(securityContext, participantContextId, participantContextId, IdentityHubParticipantContext.class)
212+
.orElseThrow(exceptionMapper(IdentityHubParticipantContext.class, participantContextId));
213+
214+
discriminatorMappings.forEach(discriminatorMappingRegistry::addMapping);
215+
}
216+
197217
}

0 commit comments

Comments
 (0)