Skip to content

Commit

Permalink
feat: implement StorageApi + validator
Browse files Browse the repository at this point in the history
  • Loading branch information
paullatzelsperger committed Feb 12, 2025
1 parent fb6fc8e commit e2fbefc
Show file tree
Hide file tree
Showing 29 changed files with 908 additions and 5 deletions.
3 changes: 2 additions & 1 deletion dist/bom/identityhub-base-bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ dependencies {
runtimeOnly(project(":core:identity-hub-participants"))
runtimeOnly(project(":core:identity-hub-keypairs"))
runtimeOnly(project(":extensions:did:local-did-publisher"))
runtimeOnly(project(":extensions:protocols:dcp:presentation-api"))
runtimeOnly(project(":extensions:protocols:dcp:dcp-identityhub:presentation-api"))
runtimeOnly(project(":extensions:protocols:dcp:dcp-identityhub:storage-api"))
runtimeOnly(project(":extensions:common:credential-watchdog"))
runtimeOnly(project(":extensions:sts:sts-account-provisioner"))
runtimeOnly(project(":extensions:api:identity-api:did-api"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class IdentityHub extends SmokeTest {
put("edc.ih.iam.publickey.path", "/some/path/to/key.pem");
put("web.http.presentation.port", valueOf(getFreePort()));
put("web.http.presentation.path", "/api/resolution");
put("web.http.storage.port", valueOf(getFreePort()));
put("web.http.storage.path", "/api/storage");
put("web.http.identity.port", valueOf(getFreePort()));
put("web.http.identity.path", "/api/identity");
put("web.http.version.port", valueOf(getFreePort()));
Expand Down Expand Up @@ -95,6 +97,8 @@ class IdentityHubWithSts extends SmokeTest {
put("edc.ih.iam.publickey.path", "/some/path/to/key.pem");
put("web.http.presentation.port", valueOf(getFreePort()));
put("web.http.presentation.path", "/api/resolution");
put("web.http.storage.port", valueOf(getFreePort()));
put("web.http.storage.path", "/api/storage");
put("web.http.identity.port", valueOf(getFreePort()));
put("web.http.identity.path", "/api/identity");
put("web.http.accounts.port", valueOf(getFreePort()));
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/runtimes/identityhub-remote-sts/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dependencies {
runtimeOnly(project(":core:identity-hub-keypairs"))
runtimeOnly(project(":extensions:did:local-did-publisher"))
runtimeOnly(project(":extensions:common:credential-watchdog"))
runtimeOnly(project(":extensions:protocols:dcp:presentation-api"))
runtimeOnly(project(":extensions:protocols:dcp:dcp-identityhub:presentation-api"))
runtimeOnly(project(":extensions:sts:sts-account-provisioner"))
runtimeOnly(project(":extensions:sts:sts-account-service-remote"))
runtimeOnly(project(":extensions:api:identity-api:did-api"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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
*
*/

plugins {
`java-library`
`maven-publish`
id("io.swagger.core.v3.swagger-gradle-plugin")
}

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)
implementation(libs.edc.spi.validator)
implementation(libs.edc.spi.web)
implementation(libs.edc.spi.dcp)
implementation(libs.edc.lib.jerseyproviders)
implementation(libs.edc.lib.transform)
implementation(libs.edc.dcp.transform)
implementation(libs.jakarta.rsApi)
testImplementation(libs.edc.junit)
testImplementation(libs.edc.jsonld)
testImplementation(testFixtures(libs.edc.core.jersey))
testImplementation(testFixtures(project(":spi:verifiable-credential-spi")))
testImplementation(libs.nimbus.jwt)
testImplementation(libs.restAssured)
}

edcBuild {
swagger {
apiGroup.set("storage-api")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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.api;

import com.fasterxml.jackson.databind.DeserializationFeature;
import org.eclipse.edc.iam.identitytrust.transform.from.JsonObjectFromPresentationResponseMessageTransformer;
import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToPresentationQueryTransformer;
import org.eclipse.edc.identityhub.api.storage.StorageApiController;
import org.eclipse.edc.identityhub.api.validation.CredentialMessageValidator;
import org.eclipse.edc.identityhub.spi.verification.SelfIssuedTokenVerifier;
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;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.runtime.metamodel.annotation.Settings;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
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.transform.transformer.edc.to.JsonValueToGenericTypeTransformer;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
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.api.StorageApiExtension.NAME;
import static org.eclipse.edc.identityhub.protocols.dcp.spi.DcpConstants.DCP_SCOPE_V_1_0;
import static org.eclipse.edc.identityhub.protocols.dcp.spi.model.CredentialMessage.CREDENTIAL_MESSAGE_TERM;
import static org.eclipse.edc.identityhub.spi.webcontext.IdentityHubApiContext.STORAGE;
import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD;

@Extension(value = NAME)
public class StorageApiExtension implements ServiceExtension {

public static final String NAME = "Storage API Extension";
private static final String API_VERSION_JSON_FILE = "storage-api-version.json";

@Configuration
private StorageApiConfiguration apiConfiguration;
@Inject
private TypeTransformerRegistry typeTransformer;
@Inject
private JsonObjectValidatorRegistry validatorRegistry;
@Inject
private WebService webService;
@Inject
private SelfIssuedTokenVerifier selfIssuedTokenVerifier;
@Inject
private JsonLd jsonLd;
@Inject
private TypeManager typeManager;
@Inject
private ApiVersionService apiVersionService;
@Inject
private PortMappingRegistry portMappingRegistry;

@Override
public String name() {
return NAME;
}

@Override
public void initialize(ServiceExtensionContext context) {
var contextString = STORAGE;

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

validatorRegistry.register(DSPACE_DCP_NAMESPACE_V_1_0.toIri(CREDENTIAL_MESSAGE_TERM), new CredentialMessageValidator());


var controller = new StorageApiController(validatorRegistry, typeTransformer, selfIssuedTokenVerifier, jsonLd);
webService.registerResource(contextString, new ObjectMapperProvider(typeManager, JSON_LD));
webService.registerResource(contextString, controller);

jsonLd.registerContext(DSPACE_DCP_V_1_0_CONTEXT, DCP_SCOPE_V_1_0);

registerTransformers(DCP_SCOPE_V_1_0, DSPACE_DCP_NAMESPACE_V_1_0);

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

void registerTransformers(String scope, JsonLdNamespace namespace) {
var scopedTransformerRegistry = typeTransformer.forContext(scope);
scopedTransformerRegistry.register(new JsonObjectToPresentationQueryTransformer(typeManager, JSON_LD, namespace));
scopedTransformerRegistry.register(new JsonValueToGenericTypeTransformer(typeManager, JSON_LD));
scopedTransformerRegistry.register(new JsonObjectFromPresentationResponseMessageTransformer(namespace));
}

private void registerVersionInfo(ClassLoader resourceClassLoader) {
try (var versionContent = resourceClassLoader.getResourceAsStream(API_VERSION_JSON_FILE)) {
if (versionContent == null) {
throw new EdcException("Version file not found or not readable.");
}
Stream.of(typeManager.getMapper()
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.readValue(versionContent, VersionRecord[].class))
.forEach(vr -> apiVersionService.addRecord("presentation", vr));
} catch (IOException e) {
throw new EdcException(e);
}
}

@Settings
record StorageApiConfiguration(
@Setting(key = "web.http." + STORAGE + ".port", description = "Port for " + STORAGE + " api context", defaultValue = 14141 + "")
int port,
@Setting(key = "web.http." + STORAGE + ".path", description = "Path for " + STORAGE + " api context", defaultValue = "/api/storage")
String path
) {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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.api.storage;

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;
import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.TYPE;

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 = "CredentialMessage", example = CredentialMessageSchema.CREDENTIALMESSAGE_EXAMPLE)
record CredentialMessageSchema(
@Schema(name = CONTEXT, requiredMode = REQUIRED)
Object context,
@Schema(name = TYPE, requiredMode = REQUIRED)
String type,
@Schema(name = "credentials", requiredMode = REQUIRED)
List<CredentialContainerSchema> credentials,
@Schema(name = "requestId", requiredMode = REQUIRED)
String requestId
) {

public static final String CREDENTIALMESSAGE_EXAMPLE = """
{
"@context": [
"https://w3id.org/dspace-dcp/v1.0/dcp.jsonld"
],
"type": "CredentialMessage",
"credentials": [
{
"credentialType": "MembershipCredential",
"payload": "",
"format": "jwt"
},
{
"credentialType": "OrganizationCredential",
"payload": "",
"format": "json-ld"
}
],
"requestId": "requestId"
}
""";
}


@Schema(name = "CredentialContainerSchema", example = CredentialContainerSchema.EXAMPLE)
record CredentialContainerSchema() {

private static final String EXAMPLE = """
{
"credentialType": "MembershipCredential",
"payload": "",
"format": "jwt"
}
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.api.storage;


import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
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;

@OpenAPIDefinition(
info = @Info(description = "This represents the Storage API as per DCP specification. It serves endpoints to write VerifiableCredentials into storage.", title = "Storage API",
version = "1"))
@SecurityScheme(name = "Authentication",
description = "Self-Issued ID token containing an access_token",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT")
public interface StorageApi {

@Tag(name = "Storage API")
@Operation(description = "Writes a set of credentials into storage",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ApiSchema.CredentialMessageSchema.class))),
responses = {
@ApiResponse(responseCode = "2xx", description = "The credentialMessage was successfully processed and stored"),
@ApiResponse(responseCode = "400", description = "Request body was malformed",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "401", description = "No Authorization header was given.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)))),
@ApiResponse(responseCode = "403", description = "The given authentication token could not be validated. This can happen, when the request body " +
"calls for a broader credentialMessage scope than the granted scope in the auth token",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class))))

}
)
Response storeCredential(String participantContextId, JsonObject credentialMessage, String authHeader);
}
Loading

0 comments on commit e2fbefc

Please sign in to comment.