Skip to content

Commit

Permalink
feat: add Resolution API (#160)
Browse files Browse the repository at this point in the history
* feat: add the Resolution API (stubbed)

* PR remarks
  • Loading branch information
paullatzelsperger authored Oct 26, 2023
1 parent 31c6df8 commit 4c5d327
Show file tree
Hide file tree
Showing 192 changed files with 2,284 additions and 9,126 deletions.
2 changes: 1 addition & 1 deletion .github/actions/gradle-setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ runs:
using: "composite"
steps:
# Install Java and cache MVD Gradle build.
- uses: actions/setup-java@v2
- uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
Expand Down
20 changes: 5 additions & 15 deletions .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,9 @@ jobs:
- name: Run Checkstyle
run: ./gradlew checkstyleMain checkstyleTest checkstyleTestFixtures

OpenAPI-Definitions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/gradle-setup

- name: Generate OpenAPI definition from code
run: ./gradlew resolve

- name: Check OpenAPI definition match code
run: git diff --exit-code

Verify-Launcher:
# disabled temporarily
if: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -79,7 +69,7 @@ jobs:
# with:
# files: "**/test-results/**/*.xml"

Component-Tests:
Integration-Tests:
runs-on: ubuntu-latest
env:
JACOCO: true
Expand All @@ -91,12 +81,12 @@ jobs:
- name: Component Tests
uses: ./.github/actions/run-tests
with:
command: ./gradlew test -DincludeTags="ComponentTest"
command: ./gradlew compileJava compileTestJava test -DincludeTags="ComponentTest,ApiTest,EndToEndTest"

Upload-Coverage-Report-To-Codecov:
needs:
- Test
- Component-Tests
- Integration-Tests
runs-on: ubuntu-latest
if: always()
steps:
Expand Down
118 changes: 62 additions & 56 deletions DEPENDENCIES

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions core/identity-hub-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* 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:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

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

dependencies {
api(libs.edc.spi.jsonld)
api(libs.edc.spi.core)
api(project(":spi:identity-hub-spi"))
implementation(project(":core:identity-hub-transform"))
implementation(libs.edc.spi.validator)
implementation(libs.edc.spi.web)
implementation(libs.edc.core.jerseyproviders)
implementation(libs.edc.core.transform)
implementation(libs.jakarta.rsApi)
testImplementation(libs.edc.junit)
testImplementation(libs.edc.ext.jsonld)
testImplementation(testFixtures(libs.edc.core.jersey))
testImplementation(testFixtures(project(":spi:identity-hub-spi")))
testImplementation(libs.nimbus.jwt)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* 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:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.identityservice.api;

import org.eclipse.edc.core.transform.transformer.to.JsonValueToGenericTypeTransformer;
import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator;
import org.eclipse.edc.identityhub.spi.model.PresentationQuery;
import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier;
import org.eclipse.edc.identityhub.transform.JsonObjectToPresentationQueryTransformer;
import org.eclipse.edc.identityservice.api.v1.PresentationApiController;
import org.eclipse.edc.identityservice.api.validation.PresentationQueryValidator;
import org.eclipse.edc.jsonld.spi.JsonLd;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.transform.spi.TypeTransformerRegistry;
import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry;
import org.eclipse.edc.web.jersey.jsonld.JerseyJsonLdInterceptor;
import org.eclipse.edc.web.jersey.jsonld.ObjectMapperProvider;
import org.eclipse.edc.web.spi.WebService;

import java.net.URISyntaxException;

import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.IATP_CONTEXT_URL;
import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.PRESENTATION_EXCHANGE_URL;
import static org.eclipse.edc.spi.CoreConstants.JSON_LD;

@Extension(value = "Presentation API Extension")
public class PresentationApiExtension implements ServiceExtension {

public static final String RESOLUTION_SCOPE = "resolution-scope";
public static final String RESOLUTION_CONTEXT = "resolution";
public static final String PRESENTATION_EXCHANGE_V_1_JSON = "presentation-exchange.v1.json";
public static final String PRESENTATION_QUERY_V_08_JSON = "presentation-query.v08.json";
@Inject
private TypeTransformerRegistry typeTransformer;

@Inject
private JsonObjectValidatorRegistry validatorRegistry;

@Inject
private WebService webService;

@Inject
private AccessTokenVerifier accessTokenVerifier;

@Inject
private CredentialQueryResolver credentialResolver;

@Inject
private PresentationGenerator presentationGenerator;

@Inject
private JsonLd jsonLd;

@Inject
private TypeManager typeManager;

@Override
public void initialize(ServiceExtensionContext context) {
// setup validator
validatorRegistry.register(PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY, new PresentationQueryValidator());


// Setup API
cacheContextDocuments(getClass().getClassLoader());
var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, presentationGenerator, context.getMonitor());

var jsonLdMapper = typeManager.getMapper(JSON_LD);
webService.registerResource(RESOLUTION_CONTEXT, new ObjectMapperProvider(jsonLdMapper));
webService.registerResource(RESOLUTION_CONTEXT, new JerseyJsonLdInterceptor(jsonLd, jsonLdMapper, RESOLUTION_SCOPE));
webService.registerResource(RESOLUTION_CONTEXT, controller);

// register transformer
typeTransformer.register(new JsonObjectToPresentationQueryTransformer(jsonLdMapper));
typeTransformer.register(new JsonValueToGenericTypeTransformer(jsonLdMapper));
}

private void cacheContextDocuments(ClassLoader classLoader) {
try {
jsonLd.registerCachedDocument(PRESENTATION_EXCHANGE_URL, classLoader.getResource(PRESENTATION_EXCHANGE_V_1_JSON).toURI());
jsonLd.registerCachedDocument(IATP_CONTEXT_URL, classLoader.getResource(PRESENTATION_QUERY_V_08_JSON).toURI());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* 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:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.identityservice.api.v1;

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

import java.util.List;

import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
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 = "PresentationQuerySchema", example = PresentationQuerySchema.PRESENTATION_QUERY_EXAMPLE)
record PresentationQuerySchema(
@Schema(name = CONTEXT, requiredMode = REQUIRED)
Object context,
@Schema(name = TYPE, requiredMode = REQUIRED)
String type,
@Schema(name = "scope", requiredMode = NOT_REQUIRED)
List<String> scope,
@Schema(name = "presentation_definition", requiredMode = NOT_REQUIRED)
PresentationDefinitionSchema presentationDefinitionSchema
) {

public static final String PRESENTATION_QUERY_EXAMPLE = """
{
"@context": [
"https://w3id.org/yourdataspace/2023/cs/v1",
"https://identity.foundation/presentation-exchange/submission/v1"
],
"@type": "Query",
"presentation_definition": null,
"scope": [
"org.eclipse.edc.vc.type:SomeCredential_0.3.5:write,
"org.eclipse.edc.vc.type:SomeOtherCredential:read,
"org.eclipse.edc.vc.type:ThirdCredential:*,
]
}
""";
}

@Schema(name = "PresentationResponse", example = PresentationResponse.RESPONSE_EXAMPLE)
record PresentationResponse() {

public static final String RESPONSE_EXAMPLE = """
""";
}

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

private static final String EXAMPLE = """
{
"comment": "taken from https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition"
"presentation_definition": {
"id": "first simple example",
"input_descriptors": [
{
"id": "A specific type of VC",
"name": "A specific type of VC",
"purpose": "We want a VC of this type",
"constraints": {
"fields": [
{
"path": [
"$.type"
],
"filter": {
"type": "string",
"pattern": "<the type of VC e.g. degree certificate>"
}
}
]
}
}
]
}
}
""";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
*
* 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:
* Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation
*
*/

package org.eclipse.edc.identityservice.api.v1;


import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn;
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;
import org.eclipse.edc.identityhub.spi.model.PresentationResponse;
import org.eclipse.edc.identityservice.api.v1.ApiSchema.ApiErrorDetailSchema;

@OpenAPIDefinition(
info = @Info(description = "This represents the Presentation API as per IATP specification. It serves endpoints to query for specific VerifiablePresentations.", title = "Resolution API",
version = "1"))
@SecurityScheme(name = "Authentication",
description = "Self-Issued ID token containing an access_token",
type = SecuritySchemeType.HTTP,
scheme = "Bearer",
bearerFormat = "JWT",
in = SecuritySchemeIn.HEADER)
public interface PresentationApi {

@Tag(name = "Resolution API")
@Operation(description = "Issues a new presentation query, that contains either a DIF presentation definition, or a list of scopes",
requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ApiSchema.PresentationQuerySchema.class), mediaType = "application/ld+json")),
responses = {
@ApiResponse(responseCode = "200", description = "The query was successfully processed, the response contains the VerifiablePresentation",
content = @Content(schema = @Schema(implementation = PresentationResponse.class), mediaType = "application/ld+json")),
@ApiResponse(responseCode = "400", description = "Request body was malformed, for example when both scope and presentation_definition are given",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "401", description = "No Authorization header was given.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "403", description = "The given authentication token could not be validated. This can happen, when the request body " +
"calls for a broader query scope than the granted scope in the auth token",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetailSchema.class)), mediaType = "application/json")),
@ApiResponse(responseCode = "501", description = "When the request contained a presentation_definition object, but the implementation does not support it.",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiErrorDetailSchema.class)), mediaType = "application/json"))
}
)
Response queryPresentation(JsonObject query, String authHeader);
}
Loading

0 comments on commit 4c5d327

Please sign in to comment.