Skip to content

Commit

Permalink
feat: introduce messages and JSON-LD transformers for DCP issuer (#551)
Browse files Browse the repository at this point in the history
  • Loading branch information
wolf4ood authored Feb 6, 2025
1 parent 0102013 commit 02723ff
Show file tree
Hide file tree
Showing 44 changed files with 1,620 additions and 131 deletions.
2 changes: 1 addition & 1 deletion dist/bom/issuerservice-base-bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies {
runtimeOnly(project(":core:issuerservice:issuerservice-participants"))
runtimeOnly(project(":extensions:did:local-did-publisher"))
// API modules
runtimeOnly(project(":extensions:protocols:dcp:issuer-api"))
runtimeOnly(project(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-api"))

runtimeOnly(project(":extensions:sts:sts-account-provisioner"))
runtimeOnly(libs.edc.identity.did.core)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ plugins {
dependencies {
api(project(":spi:identity-hub-spi"))
api(project(":spi:verifiable-credential-spi"))
api(project(":extensions:protocols:dcp:dcp-spi"))
api(libs.edc.spi.jsonld)
api(libs.edc.spi.jwt)
api(libs.edc.spi.core)
Expand All @@ -30,6 +31,7 @@ dependencies {
implementation(libs.edc.lib.jerseyproviders)
implementation(libs.edc.lib.transform)
implementation(libs.edc.dcp.transform)
implementation(project(":extensions:protocols:dcp:dcp-issuer:dcp-issuer-transform-lib"))
implementation(libs.jakarta.rsApi)
testImplementation(libs.edc.junit)
testImplementation(libs.edc.jsonld)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.credentialrequest.CredentialRequestApiController;
import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.credentialrequeststatus.CredentialRequestStatusApiController;
import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.issuermetadata.IssuerMetadataApiController;
import org.eclipse.edc.identityhub.protocols.dcp.issuer.transform.from.JsonObjectFromCredentialObjectTransformer;
import org.eclipse.edc.identityhub.protocols.dcp.issuer.transform.from.JsonObjectFromCredentialRequestStatusTransformer;
import org.eclipse.edc.identityhub.protocols.dcp.issuer.transform.from.JsonObjectFromIssuerMetadataTransformer;
import org.eclipse.edc.identityhub.protocols.dcp.issuer.transform.to.JsonObjectToCredentialRequestMessageTransformer;
import org.eclipse.edc.jsonld.spi.JsonLd;
import org.eclipse.edc.jsonld.spi.JsonLdNamespace;
import org.eclipse.edc.runtime.metamodel.annotation.Configuration;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
Expand All @@ -29,15 +35,22 @@
import org.eclipse.edc.spi.system.apiversion.ApiVersionService;
import org.eclipse.edc.spi.system.apiversion.VersionRecord;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.web.jersey.providers.jsonld.JerseyJsonLdInterceptor;
import org.eclipse.edc.web.jersey.providers.jsonld.ObjectMapperProvider;
import org.eclipse.edc.web.spi.WebService;
import org.eclipse.edc.web.spi.configuration.PortMapping;
import org.eclipse.edc.web.spi.configuration.PortMappingRegistry;

import java.io.IOException;
import java.util.stream.Stream;

import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0;
import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_V_1_0_CONTEXT;
import static org.eclipse.edc.identityhub.protocols.dcp.issuer.IssuerApiExtension.NAME;
import static org.eclipse.edc.identityhub.protocols.dcp.spi.DcpConstants.DCP_SCOPE_V_1_0;
import static org.eclipse.edc.identityhub.spi.webcontext.IdentityHubApiContext.ISSUER_API;
import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD;

@Extension(value = NAME)
public class IssuerApiExtension implements ServiceExtension {
Expand All @@ -57,18 +70,44 @@ public class IssuerApiExtension implements ServiceExtension {
@Configuration
private CredentialRequestApiConfiguration apiConfiguration;

@Inject
private TypeTransformerRegistry transformerRegistry;

@Inject
private JsonLd jsonLd;

@Override
public void initialize(ServiceExtensionContext context) {

portMappingRegistry.register(new PortMapping(ISSUER_API, apiConfiguration.port(), apiConfiguration.path()));

webService.registerResource(ISSUER_API, new CredentialRequestApiController());
webService.registerResource(ISSUER_API, new CredentialRequestStatusApiController());
webService.registerResource(ISSUER_API, new IssuerMetadataApiController());
var dcpRegistry = transformerRegistry.forContext(DCP_SCOPE_V_1_0);
registerTransformers(dcpRegistry, DSPACE_DCP_NAMESPACE_V_1_0);


webService.registerResource(ISSUER_API, new CredentialRequestApiController(dcpRegistry));
webService.registerResource(ISSUER_API, new CredentialRequestStatusApiController(dcpRegistry));
webService.registerResource(ISSUER_API, new IssuerMetadataApiController(dcpRegistry));

webService.registerResource(ISSUER_API, new ObjectMapperProvider(typeManager, JSON_LD));
webService.registerResource(ISSUER_API, new JerseyJsonLdInterceptor(jsonLd, typeManager, JSON_LD, DCP_SCOPE_V_1_0));

jsonLd.registerContext(DSPACE_DCP_V_1_0_CONTEXT, DCP_SCOPE_V_1_0);

registerVersionInfo(getClass().getClassLoader());
}

private void registerTransformers(TypeTransformerRegistry dcpRegistry, JsonLdNamespace namespace) {

// from
dcpRegistry.register(new JsonObjectFromCredentialRequestStatusTransformer(namespace));
dcpRegistry.register(new JsonObjectFromIssuerMetadataTransformer(namespace));
dcpRegistry.register(new JsonObjectFromCredentialObjectTransformer(typeManager, JSON_LD, namespace));

// to
dcpRegistry.register(new JsonObjectToCredentialRequestMessageTransformer(typeManager, JSON_LD, namespace));
}

private void registerVersionInfo(ClassLoader resourceClassLoader) {
try (var versionContent = resourceClassLoader.getResourceAsStream(API_VERSION_JSON_FILE)) {
if (versionContent == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright (c) 2025 Cofinity-X
*
* 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:
* Cofinity-X - initial API and implementation
*
*/

package org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha;

import io.swagger.v3.oas.annotations.media.Schema;

import java.util.List;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT;

public interface ApiSchema {
@Schema(name = "ApiErrorDetail", example = ApiErrorDetailSchema.API_ERROR_EXAMPLE)
record ApiErrorDetailSchema(
String message,
String type,
String path,
String invalidValue
) {
public static final String API_ERROR_EXAMPLE = """
{
"message": "error message",
"type": "ErrorType",
"path": "object.error.path",
"invalidValue": "this value is not valid"
}
""";
}

@Schema(name = "CredentialRequestMessage", example = CredentialRequestMessageSchema.RESPONSE_EXAMPLE)
record CredentialRequestMessageSchema(
@Schema(name = CONTEXT, requiredMode = REQUIRED)
Object context,
@Schema(requiredMode = REQUIRED)
List<CredentialRequestSchema> credentials
) {

public static final String RESPONSE_EXAMPLE = """
{
"@context": [
"https://w3id.org/dspace-dcp/v1.0/dcp.jsonld"
],
"type": "CredentialRequestMessage",
"credentials": [
{
"credentialType": "MembershipCredential",
"format": "vcdm11_jwt"
},
{
"credentialType": "OrganizationCredential",
"format": "vcdm11_ld"
},
{
"credentialType": "Iso9001Credential",
"format": "vcdm20_jose"
}
]
}
""";
}

@Schema(name = "CredentialRequest", example = CredentialRequestSchema.EXAMPLE)
record CredentialRequestSchema(
@Schema(name = "credentialType", requiredMode = REQUIRED)
String credentialType,
@Schema(name = "format", requiredMode = REQUIRED)
String format
) {
public static final String EXAMPLE = """
{
"credentialType": "MembershipCredential",
"format": "vcdm11_jwt"
}
""";
}

@Schema(name = "CredentialStatus", example = CredentialStatusSchema.RESPONSE_EXAMPLE)
record CredentialStatusSchema(
@Schema(name = CONTEXT, requiredMode = REQUIRED)
Object context,
@Schema(name = "type", requiredMode = REQUIRED)
String type,
@Schema(name = "status", requiredMode = REQUIRED)
String status
) {

public static final String RESPONSE_EXAMPLE = """
{
"@context": [
"https://w3id.org/dspace-dcp/v1.0/dcp.jsonld"
],
"type": "CredentialStatus",
"requestId": "requestId",
"status": "RECEIVED"
}
""";
}

@Schema(name = "IssuerMetadata", example = IssuerMetadataSchema.RESPONSE_EXAMPLE)
record IssuerMetadataSchema(
@Schema(name = CONTEXT, requiredMode = REQUIRED)
Object context,
@Schema(name = "type", requiredMode = REQUIRED)
String type,
@Schema(name = "credentialIssuer", requiredMode = REQUIRED)
String credentialIssuer,
@Schema(name = "status", requiredMode = REQUIRED)
String status
) {

public static final String RESPONSE_EXAMPLE = """
{
"@context": [
"https://w3id.org/dspace-dcp/v1.0/dcp.jsonld"
],
"type": "IssuerMetadata",
"credentialIssuer": "did:web:issuer-url",
"credentialsSupported": [
{
"type": "CredentialObject",
"credentialType": "MembershipCredential",
"offerReason": "reissue",
"bindingMethods": [
"did:web"
],
"profiles": [
"vc20-bssl/jwt", "vc10-sl2021/jwt", "..."
],
"issuancePolicy": {
"id": "Scalable trust example",
"input_descriptors": [
{
"id": "pd-id",
"constraints": {
"fields": [
{
"path": [
"$.vc.type"
],
"filter": {
"type": "string",
"pattern": "^AttestationCredential$"
}
}
]
}
}
]
}
}
]
}
""";
}

@Schema(name = "CredentialObject", example = CredentialObjectSchema.EXAMPLE)
record CredentialObjectSchema(
@Schema(name = "credentialType", requiredMode = REQUIRED)
String credentialType,
@Schema(name = "format", requiredMode = REQUIRED)
String format
) {
public static final String EXAMPLE = """
{
"credentialType": "MembershipCredential",
"format": "vcdm11_jwt"
}
""";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.json.JsonObject;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.ApiSchema;

Expand All @@ -42,10 +43,10 @@ public interface CredentialRequestApi {
@Tag(name = "Credential Request API")
@Operation(description = "Requests the issuance of one or several verifiable credentials from an issuer",
operationId = "requestCredentials",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = CredentialRequestMessage.class))),
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ApiSchema.CredentialRequestMessageSchema.class))),
responses = {
@ApiResponse(responseCode = "201", description = "The request was successfully received and is being processed.", headers = {@Header(name = "Location",
description = "contains the relative URL where the status of the request can be queried (Credential Request Status API)")}),
@ApiResponse(responseCode = "201", description = "The request was successfully received and is being processed.", headers = { @Header(name = "Location",
description = "contains the relative URL where the status of the request can be queried (Credential Request Status API)") }),
@ApiResponse(responseCode = "400", description = "Request body was malformed, e.g. required parameter or properties were missing",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "401", description = "No Authorization header was provided.",
Expand All @@ -55,5 +56,5 @@ public interface CredentialRequestApi {

}
)
Response requestCredential(CredentialRequestMessage message);
Response requestCredential(JsonObject message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

package org.eclipse.edc.identityhub.protocols.dcp.issuer.api.v1alpha.credentialrequest;

import jakarta.json.JsonObject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;

import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;

Expand All @@ -27,10 +29,14 @@
@Path("/v1alpha/credentials")
public class CredentialRequestApiController implements CredentialRequestApi {

public CredentialRequestApiController(TypeTransformerRegistry dcpRegistry) {

}

@POST
@Path("/")
@Override
public Response requestCredential(CredentialRequestMessage message) {
public Response requestCredential(JsonObject message) {
return Response.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ public interface CredentialRequestStatusApi {
@Tag(name = "Credential Request Status API")
@Operation(description = "Requests status information about an issuance request from an issuer",
operationId = "getCredentialRequestStatus",
parameters = {@Parameter(name = "credentialRequestId", description = "ID of the Credential Request that was sent previously", required = true, in = ParameterIn.PATH)},
parameters = { @Parameter(name = "credentialRequestId", description = "ID of the Credential Request that was sent previously", required = true, in = ParameterIn.PATH) },
responses = {
@ApiResponse(responseCode = "200", description = "Gets the status of a credentials request.",
content = @Content(schema = @Schema(implementation = CredentialRequestStatus.class))),
content = @Content(schema = @Schema(implementation = ApiSchema.CredentialStatusSchema.class))),
@ApiResponse(responseCode = "400", description = "Request was malformed, e.g. required parameter or properties were missing",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "401", description = "No Authorization header was provided.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Response;

import java.time.Instant;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;

import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;

Expand All @@ -30,18 +29,18 @@
@Path("/v1alpha/requests")
public class CredentialRequestStatusApiController implements CredentialRequestStatusApi {

public CredentialRequestStatusApiController(TypeTransformerRegistry dcpRegistry) {

}

@GET
@Path("/{credentialRequestId}")
@Override
public Response requestCredential(@PathParam("credentialRequestId") String credentialRequestId) {
if (credentialRequestId == null || credentialRequestId.isEmpty()) {
return Response.status(400).build();
}
return Response.ok(CredentialRequestStatus.Builder.newInstance()
.message("dummy-message")
.requestId("dummy-request-id")
.requestStatus("RECEIVED")
.timestamp(Instant.now()).build())
return Response.ok()
.build();
}
}
Loading

0 comments on commit 02723ff

Please sign in to comment.