Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce messages and JSON-LD transformers for DCP issuer #551

Merged
merged 1 commit into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should utilize the externalDoc functionality of OpenAPI to directly reference relevant parts of the DCP specification...
for a separate PR of course, but that would guarantee consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also remove the OpenApi docs and use directly the specification as we do for DSP

Copy link
Member

@paullatzelsperger paullatzelsperger Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm OK with either

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);
Dismissed Show dismissed Hide dismissed
}
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) {
Dismissed Show dismissed Hide dismissed

}

@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) {

Check notice

Code scanning / CodeQL

Useless parameter Note

The parameter 'dcpRegistry' is never used.

}

@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