diff --git a/README.md b/README.md index b8c33838f7256b25ddd376b944f2f5bf14377e6e..e298c5a4dd59203795ed5521d05baeb3656d6467 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ _The following steps show how to get the SDK running_ ```sh ./mvnw clean install -DskipTests ``` -5. Configure your `config.yml` +5. Configure your `config.yml` (see [template](/config.yml)) - add Sender OAuth credentials from the self-service portal to *SENDER* section - add Subscriber OAuth credentials from the self-service portal to *SUBSCRIBER* section - add reference to private decryption key (JWK) to *SUBSCRIBER* section @@ -110,6 +110,59 @@ _The following steps show how to get the SDK running_ <p align="right">(<a href="#top">back to top</a>)</p> +## API Usage for Routing +The Routing-Client allows to retrieve data about areas and services as well as their service destination. +A typical workflow using the `RoutingClient` and `SenderClient` would be: + +1) Find the `areaId` for an area via routing +2) Find the destination for a `leikaKey` and an `areaId` via routing +3) Submit a new submission to the destination using the `SenderClient` + +### Finding Areas +Areas can be searched with one or more search criteria: + +```java +final RoutingClient routingClient = ClientFactory.routingClient(config); + +final var citySearchCriterion = "Leip*"; +final var zipCodeSearchCriterion = "04229"; + +// get first 5 area results +final List<Area> areas = routingClient.findAreas(List.of(citySearchCriterion, zipCodeSearchCriterion), 0, 5); + +LOGGER.info("Found {} areas", areas.size()); +for (final Area area : areas){ + LOGGER.info("Area {} with id {} found", area.getName(), area.getId()); +} +``` + +### Finding Destinations by service identifier and region + +For searching a destination the `DestinationSearch.Builder` is used pass a search request to the routing client. +The leikaKey is mandatory, as well as (max.) one other search criterion for the area/region, like one of: +- ars amtlicher regionalschlüssel +- ags amtlicher gemeindeschlüssel +- areaId identifier of an area that can be retrieved via [finding areas](#finding-areas) + +```java +final RoutingClient routingClient = ClientFactory.routingClient(config); + +final DestinationSearch search = DestinationSearch.Builder() + .withLeikaKey("99123456760610") + .withArs("064350014014") + .withLimit(5) + .build(); + +// get first 5 route results +final List<Route> routes = routingClient.findDestinations(search); + +LOGGER.info("Found {} routes for service identifier {}", routes.size(), leikaKey); +for (final Route route : routes){ + LOGGER.info("Route {} with destinationId {} found", route.getName(), route.getDestinationId()); +} +``` + + ## API Usage for Sender For sending submission and already encrypted submissions builder are provided to construct the necessary payload to be sent. @@ -374,8 +427,7 @@ if(validationResult.hasError()){ ``` ## Roadmap -- [ ] Add Routing features -- [ ] Add Callback validation + - [ ] Add auto-reject on technical errors - [ ] Maven central release of 1.0.0 diff --git a/api/README.md b/api/README.md index 9981df66fdc0e09d84f8ee3e8748b8a51fdab368..18ff35050c164b4c952526ca5e31906974d21e35 100644 --- a/api/README.md +++ b/api/README.md @@ -1,7 +1,6 @@ ## API Module -The API-module contains interfaces and domain model value classes that provide the basic functionality to build an -sdk-client. +The API-module contains interfaces and domain model value classes that provide the basic functionality to build a sdk-client. ### Structure @@ -10,7 +9,7 @@ sdk-client. - **api.exceptions** - all use case specific the services throw - **api.services** - all services to authenticate, encrypt, validate and perform REST-requests -There are two service facade interfaces that provide a client centric wrapper around the underlying services, +There are two service facade interfaces that provide a client centric wrapper around the underlying services: - **Sender** - create a submission, announce attachments, encrypt and send the submission including metadata - **Subscriber** - poll, receive and decrypt submissions and confirm their valid transmission diff --git a/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java b/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java index deeb94b053a444797f00886b5f4ac58b7cd72c02..5c57e4aa7fa47ba9f2d40b0697ab5c34537e91d9 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/config/ApplicationConfig.java @@ -30,6 +30,9 @@ public class ApplicationConfig { @Builder.Default private URI metadataSchemaWriteVersion = SchemaConfig.METADATA_V_1_0_0.getSchemaUri(); + @Builder.Default + private URI destinationSchemaVersion = SchemaConfig.XZUFI_DESTINATION_SCHEMA.getSchemaUri(); + private SenderConfig senderConfig; private SubscriberConfig subscriberConfig; diff --git a/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java b/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java index 96b2c3d78fa139b782dcf369ac1059e932aec3ae..0f429301b2ddad6434f366c1a383746a7edf4352 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/config/ResourcePaths.java @@ -6,14 +6,19 @@ final class ResourcePaths { } static final String AUTH_TOKEN_PATH = "/token"; + static final String DESTINATIONS_PATH = "/v1/destinations/{destinationId}"; static final String DESTINATIONS_KEY_PATH = "/v1/destinations/{destinationId}/keys/{kid}"; + static final String EVENTS_PATH = "/v1/cases/{caseId}/events"; + static final String SUBMISSION_PATH = "/v1/submissions/{submissionId}"; static final String SUBMISSIONS_PATH = "/v1/submissions"; static final String SUBMISSION_ATTACHMENT_PATH = "/v1/submissions/{submissionId}/attachments/{attachmentId}"; + static final String ROUTING_AREA_PATH = "/v1/areas"; static final String ROUTING_ROUTE_PATH = "/v1/routes"; + static final String WELL_KNOWN_KEYS_PATH = "/.well-known/jwks.json"; } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java b/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java index ca64a9591950639ca6bc6481fd1e9d62c281bc5c..eeffbfb7dce1506b1c6af3e8229df2183f644955 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/config/SchemaConfig.java @@ -14,7 +14,9 @@ public enum SchemaConfig { EVENTS_SCHEMA_PATH(SCHEMA_BASE_URL.schemaUri.resolve("events/")), SET_V_1_0_1(SCHEMA_BASE_URL.schemaUri.resolve("set-payload/1.0.1/set-payload.schema.json"), "set_schema_1.0.1.json"), SET_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("set-payload/1.0.0/set-payload.schema.json"), "set_schema_1.0.0.json"), - METADATA_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("metadata/1.0.0/metadata.schema.json"), "metadata_schema_1.0.0.json"); + METADATA_V_1_0_0(SCHEMA_BASE_URL.schemaUri.resolve("metadata/1.0.0/metadata.schema.json"), "metadata_schema_1.0.0.json"), + + XZUFI_DESTINATION_SCHEMA(SCHEMA_BASE_URL.schemaUri.resolve("xzufi/destination.schema.json"), "destination_schema.json"); private final URI schemaUri; @@ -42,6 +44,12 @@ public enum SchemaConfig { .collect(Collectors.toList()); } + public static List<String> getDestinationSchemaPaths(final String destinationSchemaBaseDir) { + return Stream.of(XZUFI_DESTINATION_SCHEMA.fileName) + .map(fileName -> destinationSchemaBaseDir + "/" + fileName) + .collect(Collectors.toList()); + } + @Override public String toString() { return schemaUri.toString(); diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java index ed32bc9179829fd836574367d471c55b3681b335..7f9fec808762c485d58af6375dca265e66ba41d1 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/destination/ReplyChannelEMail.java @@ -1,14 +1,12 @@ package dev.fitko.fitconnect.api.domain.model.destination; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import lombok.Data; import lombok.NoArgsConstructor; @Data -@JsonTypeName("ReplyChannel_eMail") @NoArgsConstructor -public class ReplyChannelEMail { +class ReplyChannelEMail { @JsonProperty("usePgp") private Boolean usePgp; diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java index 5e12c15d01b2218be1dc1f99ac9bdacd9e6eb25e..553fd977469a2af835580bc8cd346c6850f0f1eb 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/Purpose.java @@ -3,34 +3,45 @@ package dev.fitko.fitconnect.api.domain.model.metadata.attachment; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; -import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; public enum Purpose { FORM("form"), ATTACHMENT("attachment"), REPORT("report"); + private final String value; + private static final Map<String, Purpose> CONSTANTS = new HashMap<>(); + + static { + for (final Purpose p : values()) { + CONSTANTS.put(p.value, p); + } + } + Purpose(final String value) { this.value = value; } @Override public String toString() { - return this.value; + return value; } @JsonValue public String value() { - return this.value; + return value; } @JsonCreator public static Purpose fromValue(final String value) { - return Arrays.stream(Purpose.values()) - .filter(enumValue -> enumValue.value.equals(value)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException("Unexpected value '" + value + "'")); + final Purpose constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + return constant; } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Area.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Area.java new file mode 100644 index 0000000000000000000000000000000000000000..5bd59c70e123822d623ac37dd0dc5cbff7bb8fe6 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Area.java @@ -0,0 +1,20 @@ +package dev.fitko.fitconnect.api.domain.model.route; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Area { + + @JsonProperty("id") + private String id; + + @JsonProperty("name") + private String name; + + @JsonProperty("type") + private String type; + +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/AreaResult.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/AreaResult.java new file mode 100644 index 0000000000000000000000000000000000000000..7943a6ce33adfb03e735ba73f360f79531aae18d --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/AreaResult.java @@ -0,0 +1,27 @@ +package dev.fitko.fitconnect.api.domain.model.route; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AreaResult { + + @JsonProperty("count") + private Integer count; + + @JsonProperty("offset") + private Integer offset; + + @JsonProperty("totalCount") + private Integer totalCount; + + @JsonProperty("areas") + private List<Area> areas = new ArrayList<>(); +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Route.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Route.java new file mode 100644 index 0000000000000000000000000000000000000000..0bd6fd3741e2214fdddc681cee8b912fdc4a1632 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/Route.java @@ -0,0 +1,31 @@ +package dev.fitko.fitconnect.api.domain.model.route; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.UUID; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Route { + + @JsonProperty("destinationId") + private UUID destinationId; + + @JsonProperty("destinationSignature") + private String destinationSignature; + + @JsonProperty("destinationParameters") + private RouteDestination destinationParameters; + + @JsonProperty("destinationParametersSignature") + private String destinationParametersSignature; + + @JsonProperty("destinationName") + private String destinationName; + + @JsonProperty("destinationLogo") + private String destinationLogo; + +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteDestination.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteDestination.java new file mode 100644 index 0000000000000000000000000000000000000000..8db7011a36135a10fa6d3d1ce169bc4b2142d67c --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteDestination.java @@ -0,0 +1,42 @@ +package dev.fitko.fitconnect.api.domain.model.route; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import dev.fitko.fitconnect.api.domain.model.destination.ReplyChannel; +import dev.fitko.fitconnect.api.domain.model.destination.StatusEnum; +import dev.fitko.fitconnect.api.domain.model.jwk.ApiJwkSet; +import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RouteDestination { + + @JsonProperty("encryptionKid") + private String encryptionKid = null; + + @JsonProperty("metadataVersions") + private List<String> metadataVersions = new ArrayList<>(); + + @JsonProperty("publicKeys") + private ApiJwkSet publicKeys = null; + + @JsonProperty("replyChannels") + private ReplyChannel replyChannels = null; + + @JsonProperty("status") + private StatusEnum status = null; + + @JsonProperty("submissionSchemas") + private List<SubmissionSchema> submissionSchemas = new ArrayList<>(); + + @JsonProperty("submissionUrl") + private String submissionUrl = null; + + + + +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteResult.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteResult.java new file mode 100644 index 0000000000000000000000000000000000000000..0fafa2fb6d90c7eeec586ba2d4d56d39e3231393 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/route/RouteResult.java @@ -0,0 +1,28 @@ +package dev.fitko.fitconnect.api.domain.model.route; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RouteResult { + + @JsonProperty("count") + private Integer count; + + @JsonProperty("offset") + private Integer offset; + + @JsonProperty("totalCount") + private Integer totalCount; + + @JsonProperty("routes") + private List<Route> routes = new ArrayList<>(); + +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/schema/SchemaResources.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/schema/SchemaResources.java new file mode 100644 index 0000000000000000000000000000000000000000..022b164a45561ce5305734532cc14a518787625f --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/schema/SchemaResources.java @@ -0,0 +1,13 @@ +package dev.fitko.fitconnect.api.domain.schema; + +import lombok.Value; + +import java.util.List; + +@Value +public class SchemaResources { + + List<String> setSchemaPaths; + List<String> metadataSchemaPaths; + List<String> destinationSchemaPaths; +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RoutingException.java b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RoutingException.java new file mode 100644 index 0000000000000000000000000000000000000000..ad334471f971fc361883cc03781fdd37b6b21f36 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RoutingException.java @@ -0,0 +1,12 @@ +package dev.fitko.fitconnect.api.exceptions; + +public class RoutingException extends RuntimeException { + + public RoutingException(final String errorMessage) { + super(errorMessage); + } + + public RoutingException(final String errorMessage, final Throwable error) { + super(errorMessage, error); + } +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java index 153b0614cef0d932e9e3ff991810060529beeb5a..040dc4166d8c5b001ff46a63f5ac566f432e11c6 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/keys/KeyService.java @@ -38,7 +38,7 @@ public interface KeyService { * @param keyId unique identifier of the {@link RSAKey} * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)}) */ - RSAKey getPortalSignatureKey(String keyId); + RSAKey getPortalPublicKey(String keyId); /** * Get a public signature key for a given key-id from the submission service well-known keys. @@ -46,6 +46,15 @@ public interface KeyService { * @param keyId unique identifier of the {@link RSAKey} * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)}) */ - RSAKey getSubmissionServiceSignatureKey(String keyId); + RSAKey getSubmissionServicePublicKey(String keyId); + /** + * Get a public signature key for a given key-id from a submission service instance url well known keys. + * The <b>'/well-known/jkws.json'</b> is added to the given url. + * + * @param url custom url to load the well known keys from + * @param keyId unique identifier of the {@link RSAKey} the well known keys are filtered by + * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)}) + */ + RSAKey getWellKnownKeysForSubmissionUrl(String url, String keyId); } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingService.java new file mode 100644 index 0000000000000000000000000000000000000000..d398df4511dffc54be443b6da13ab7608169fa11 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingService.java @@ -0,0 +1,42 @@ +package dev.fitko.fitconnect.api.services.routing; + +import dev.fitko.fitconnect.api.domain.model.route.Area; +import dev.fitko.fitconnect.api.domain.model.route.AreaResult; +import dev.fitko.fitconnect.api.domain.model.route.Route; +import dev.fitko.fitconnect.api.domain.model.route.RouteResult; +import dev.fitko.fitconnect.api.exceptions.RestApiException; + +import java.util.List; + +/** + * Routing API Service that retrieves data form FIT-Connect Routing API REST-Endpoints + */ +public interface RoutingService { + + /** + * Finds an {@link AreaResult} based on a list of multiple filter criteria include a zip code or e.g. city as in List.of("04229", "Leip*"). + * + * @param searchExpressions list of string filters + * @param offset offset to start from + * @param limit max entries + * + * @return list of {@link Area} + * @throws RestApiException if a technical error occurred + */ + AreaResult getAreas(List<String> searchExpressions, int offset, int limit) throws RestApiException; + + /** + * Finds a {@link RouteResult} by a given service identifier and AT LEAST ONE OTHER search criterion (ars | ags | areaId). + * + * @param leikaKey leikaKey + * @param ars amtlicher regionalschlüssel + * @param ags amtlicher gemeindeschlüssel + * @param areaId areaId + * @param offset offset to start from + * @param limit max entries + * + * @return list of found {@link Route}s matching the search criteria + * @throws RestApiException if a technical error occurred + */ + RouteResult getRoutes(String leikaKey, String ars, String ags, String areaId, int offset, int limit) throws RestApiException; +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingVerificationService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingVerificationService.java new file mode 100644 index 0000000000000000000000000000000000000000..7f7ca0af06d071ea59573e40a2aaaaac9586ec2e --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/routing/RoutingVerificationService.java @@ -0,0 +1,22 @@ +package dev.fitko.fitconnect.api.services.routing; + +import dev.fitko.fitconnect.api.domain.model.route.Route; +import dev.fitko.fitconnect.api.domain.validation.ValidationResult; + +import java.util.List; + +/** + * Service to verify the validity of routing-related data. + */ +public interface RoutingVerificationService { + + /** + * Verifies host url, self-service portal signature and DVDV signature of the provided {@link Route}. + * + * @param routes list of routes to be verified + * @param requestedServiceIdentifier requested service identifier to be checked if the route supports it + * @param requestedRegion requested service region to be checked if the routes supports it + * @return {@link ValidationResult}, is ok if validation passes, has an error including an exception if the validation failed + */ + ValidationResult validateRouteDestinations(List<Route> routes, String requestedServiceIdentifier, String requestedRegion); +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java b/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java index 70cc8bdd31f2274078a731ed73ce10c4e3c6285c..95bf3bedc5aef606bc12b404d3d83e031949ccc3 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/schema/SchemaProvider.java @@ -50,4 +50,14 @@ public interface SchemaProvider { * @throws SchemaNotFoundException if the schema is not existing */ String loadMetadataSchema(URI schemaUri) throws SchemaNotFoundException; + + /** + * Load the schema for payload of signed destination. + * + * @param schemaUri uri of the destination schema that should be loaded + * @return the schema as string + * + * @throws SchemaNotFoundException if the schema is not existing + */ + String loadDestinationSchema(URI schemaUri) throws SchemaNotFoundException; } \ No newline at end of file diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java index 34c97b6660919e65a9281fbed6d7a9895df49dd0..c5d747b3f9e54e225c6f9ef0d55aad86628c4f4d 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/validation/ValidationService.java @@ -4,6 +4,8 @@ import com.nimbusds.jose.jwk.RSAKey; import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import java.util.Map; + /** * Validator for publicKeys and metadata. * @@ -53,6 +55,15 @@ public interface ValidationService { */ ValidationResult validateSetEventSchema(String setEventPayload); + /** + * Validates signature payload claims against a given schema. + * + * @param destinationPayload the payload to be validated + * + * @return a {@link ValidationResult} with an optional error + */ + ValidationResult validateDestinationSchema(Map<String, Object> destinationPayload); + /** * Compares a given byte[] to its original hash value. * diff --git a/client/README.md b/client/README.md index 34acfc52addd637b84b765b6ea9d91753522e9a7..9d9ca485e698761807456e1560309695298408cf 100644 --- a/client/README.md +++ b/client/README.md @@ -1,11 +1,11 @@ ## Client Module ### Commandline Client -The sdk comes with a commandline client to be able to use the sdk without any coding. +The sdk provides a commandline-client as a runnable .jar, to be able to use the sdk without any coding. #### Setup & Build 1. Build project root wih ``./mvnw clean package`` -2. Go to client/target and find a runnable jar ``client-VERSION.jar`` +2. Go to client/target and find a runnable jar ``fit-connect-client.jar`` 3. Provide [config yaml](../config.yml): 1. set environment variable ``FIT_CONNECT_CONFIG``: 1. Linux/MacOS: ``export FIT_CONNECT_CONFIG=path/to/config.yml`` @@ -16,13 +16,13 @@ The sdk comes with a commandline client to be able to use the sdk without any co var senderClient = ClientFactory.senderClient(config); ```` 3. put ``config.yml`` in same path as the .jar-file -5. run client with ``java -jar client-VERSION.jar [COMMAND] [OPTIONS]`` +5. run client with ``java -jarfit-connect-client.jar [COMMAND] [OPTIONS]`` -#### SEND Example +#### SEND Single Submission Example The send command submits a new submission to a destination. Apart from optional attachments, all options are mandatory. ````sh -java -jar client-1.0-SNAPSHOT.jar send +java -jar fit-connect-client.jar send --destinationId=1b7d1a24-a6c8-4050-bb71-ae3749ec432f --serviceName=Test --leikaKey=urn:de:fim:leika:leistung:99400048079000 @@ -35,7 +35,7 @@ java -jar client-1.0-SNAPSHOT.jar send #### LIST Submissions Example The list command lists all submissionIds for a given destinationID. ````sh -java -jar client-1.0-SNAPSHOT.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f +java -jar fit-connect-client.jar list --destinationID=1b7d1a24-a6c8-4050-bb71-ae3749ec432f ```` #### GET Single Submission Example The get command loads a submission by `submissionId` and stores data and attachments in the given target location. If no target is @@ -43,13 +43,13 @@ set, the cmd-client saves the data into in a folder named by the `submissionId` jar. ````sh -java -jar client-1.0-SNAPSHOT.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data +java -jar fit-connect-client.jar get --submissionID=cc9b9b3c-d4b1-4ac7-a70b-e7ca76e88608 --target=Users/submissions/data ```` #### Batch Mode To send multiple submission with a single command, the client can be used in batch mode: ````sh -java -jar client-1.0-SNAPSHOT.jar batch --data=batch_data.csv +java -jar fit-connect-client.jar batch --data=batch_data.csv ```` Currently, the import of CSV is supported. Follow the schema below, setting up your data: @@ -112,95 +112,3 @@ Usage: <main class> [command] [command options] * --data Path to submission data as csv ```` - -### API Flow - -The ClientFactory provides fluent API clients for both **Sender** and **Subscriber**. - -As the flow chart below shows, the fluent client guides through all essential calls in order to hand in a correct -**submission** as well as receive submissions on the subscriber side. - -#### Api client flow for sending a submission - -For the actual sender client those calls look like this: - - -<table> -<tr> -<th>Workflow</th> -<th>Java sample calls</th> -</tr> -<tr> -<td> - -```mermaid - -flowchart TD - -A[Create Client] --> B(Add Attachments) -A[Create Client] --> C(Add Data) -B -->|next| C[Add Data] -C -->|next| D[Add Destination] -D -->|next| E[Add ServiceType] -E -->|send| F[SubmissionForPickup] -``` -Add Service Type -</td> -<td> - -```java - -ClientFactory.senderClient() - .withAttachments(attachments) - .withJsonData("{ caseSpecific: 'Data' }") - .withDestination(UUID.randomUUID()) - .withServiceType("ServiceName", "leika:key:service") - .submit(); -``` - -</td> -</tr> -</table> - -#### Api client flow for subscribing to a submission - -<table> -<tr> -<th>Workflow</th> -<th>Java sample calls</th> -</tr> -<tr> -<td> - -```mermaid -flowchart TD - -A[Create Client] --> B(Poll List ofAvailable Submissions) -A[Create Client] --> C(Request Submission by ID) - -C -->|get| D[Attachments] -C -->|get| E[Metadata] -C -->|get| F[Data] - -``` - -</td> -<td> - -```java - - var client = ClientFactory.subscriberClient(); - - var submissions = client.getAvailableSubmissions(destinationId); - // filter submission list for requested one ... - - var submission = client.requestSubmission(submissionId) - submission.getAttachments(); - submission.getMetadata(); - submission.getData(); -``` - -</td> -</tr> -</table> - diff --git a/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java b/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java new file mode 100644 index 0000000000000000000000000000000000000000..f91e6e2956ab541239f57752b93247855fcb27f1 --- /dev/null +++ b/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java @@ -0,0 +1,68 @@ +package dev.fitko.fitconnect.client; + +import dev.fitko.fitconnect.api.domain.model.route.Area; +import dev.fitko.fitconnect.api.domain.model.route.Route; +import dev.fitko.fitconnect.api.domain.model.route.RouteResult; +import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.exceptions.RoutingException; +import dev.fitko.fitconnect.api.services.routing.RoutingService; +import dev.fitko.fitconnect.client.router.DestinationSearch; +import dev.fitko.fitconnect.core.routing.RouteVerifier; + +import java.util.List; + +public final class RoutingClient { + + private final RoutingService routingService; + private final RouteVerifier routeVerifier; + + public RoutingClient(final RoutingService routingService, final RouteVerifier routeVerifier) { + this.routingService = routingService; + this.routeVerifier = routeVerifier; + } + + /** + * Finds a list of {@link Route}s by a given service identifier and an area search criterion. + * + * @param search search parameters that contain leikaKey and one other area search criterion see {@link DestinationSearch} + * @return list of found {@link Route}s matching the search criteria + * @throws RoutingException if the signature validation failed + * @throws RestApiException if a technical error occurred during the query + */ + + public List<Route> findDestinations(final DestinationSearch search) throws RoutingException, RestApiException { + final RouteResult routeResult = routingService.getRoutes(search.getLeikaKey(), search.getArs(), search.getAgs(), search.getAreaId(), search.getOffset(), search.getLimit()); + final ValidationResult result = routeVerifier.validateRouteDestinations(routeResult.getRoutes(), search.getLeikaKey(), search.getArs()); + if (result.hasError()) { + throw new RoutingException(result.getError().getMessage(), result.getError()); + } + return routeResult.getRoutes(); + } + + /** + * Finds a list of {@link Area}s based on a filter criterion, e.g. a zip code or city. + * + * @param filter filter criterion as string + * @param offset offset to start from + * @param limit max entries + * @return list of {@link Area} + * @throws RestApiException if a technical error occurred during the query + */ + public List<Area> findAreas(final String filter, final int offset, final int limit) throws RestApiException { + return findAreas(List.of(filter), offset, limit); + } + + /** + * Find a list {@link Area}s based on a list of multiple filter criteria, e.g. a zip code or a city as in List.of("04229", "Leip*"). + * + * @param filters list of string filters + * @param offset offset to start from + * @param limit max entries + * @return list of {@link Area} + * @throws RestApiException if a technical error occurred during the query + */ + public List<Area> findAreas(final List<String> filters, final int offset, final int limit) throws RestApiException { + return routingService.getAreas(filters, offset, limit).getAreas(); + } +} diff --git a/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java b/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java index 72d563b702b3780a9f90d356f616d8ca1d2f22c2..523193e4379bd075df2c4a7e5dd5a517df80fe79 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/SenderClient.java @@ -21,7 +21,7 @@ import java.util.Optional; import java.util.UUID; /** - * A fluent client for announcing and handing in a {@link SubmitSubmission} + * A sender-side client for announcing and handing in a {@link SubmitSubmission} */ public class SenderClient { @@ -77,7 +77,7 @@ public class SenderClient { * @param timestamp timestamp provided by the callback * @param httpBody HTTP body provided by the callback * @param callbackSecret secret owned by the client, which is used to calculate the hmac - * @return {@code true} if hmac and timestamp provided by the callback meet the required conditions + * @return {@code ValidationResult.ok()} if hmac and timestamp provided by the callback meet the required conditions */ public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) { return sender.validateCallback(hmac, timestamp, httpBody, callbackSecret); diff --git a/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java b/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java index a2caf86626e654eb3ae04eaadd8bf4d19ddec140..4c75814f737c024c901915f335fd93289a7d6b27 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java @@ -31,11 +31,10 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildDecryptedAttachmentPayload; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.getDataHashFromMetadata; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.mapToReceivedAttachments; /** * A subscriber-side client for retrieving submissions. @@ -144,17 +143,27 @@ public class SubscriberClient { return null; } - public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) { - return this.subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret); + public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) { + return subscriber.validateCallback(hmac, timestamp, httpBody, callbackSecret); } private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List<DecryptedAttachmentPayload> attachments) { final MimeType mimeType = metadata.getContentStructure().getData().getSubmissionSchema().getMimeType(); final ReceivedData receivedData = new ReceivedData(new String(decryptedData, StandardCharsets.UTF_8), mimeType); - final List<ReceivedAttachment> receivedAttachments = mapToReceivedAttachments(attachments); + final List<ReceivedAttachment> receivedAttachments = attachments.stream().map(mapToReceivedAttachment()).collect(Collectors.toList()); return new ReceivedSubmission(subscriber, submission, metadata, receivedData, receivedAttachments); } + private static Function<DecryptedAttachmentPayload, ReceivedAttachment> mapToReceivedAttachment() { + return payload -> ReceivedAttachment.builder() + .attachmentId(payload.getAttachmentMetadata().getAttachmentId()) + .filename(payload.getAttachmentMetadata().getFilename()) + .mimeType(payload.getAttachmentMetadata().getMimeType()) + .description(payload.getAttachmentMetadata().getDescription()) + .data(payload.getDecryptedContent()) + .build(); + } + private List<DecryptedAttachmentPayload> loadAttachments(final UUID submissionId, final List<Attachment> attachmentMetadata) { if (attachmentMetadata == null || attachmentMetadata.isEmpty()) { LOGGER.info("Submission contains no attachments"); @@ -164,7 +173,11 @@ public class SubscriberClient { for (final Attachment metadata : attachmentMetadata) { final String encryptedAttachment = downloadAttachment(submissionId, metadata); final byte[] decryptedAttachment = decryptAttachment(metadata, encryptedAttachment); - receivedAttachments.add(buildDecryptedAttachmentPayload(metadata, decryptedAttachment)); + final DecryptedAttachmentPayload decryptedAttachmentPayload = DecryptedAttachmentPayload.builder() + .decryptedContent(decryptedAttachment) + .attachmentMetadata(metadata) + .build(); + receivedAttachments.add(decryptedAttachmentPayload); } return receivedAttachments; } @@ -207,4 +220,10 @@ public class SubscriberClient { return ValidationResult.ok(); } + private String getDataHashFromMetadata(final Metadata metadata) { + return metadata.getContentStructure() + .getData() + .getHash() + .getContent(); + } } diff --git a/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java b/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java index 706c23a45306ffa6b5f33512813dbf8606cfd89a..d6c67ce20d9cca49a4dbde93192007344e6c5b8c 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/factory/ApplicationConfigLoader.java @@ -52,11 +52,16 @@ public final class ApplicationConfigLoader { public static ApplicationConfig loadConfigFromYaml(final String configYaml) { final Yaml applicationPropertiesYaml = new Yaml(new Constructor(ApplicationConfig.class)); - ApplicationConfig applicationConfig = applicationPropertiesYaml.load(configYaml); + final ApplicationConfig applicationConfig = applicationPropertiesYaml.load(configYaml); return applicationConfig; } + /** + * Load BuildInfo properties. + * + * @return {@link BuildInfo} + */ public static BuildInfo loadBuildInfo() { final Yaml buildInfoYaml = new Yaml(new Constructor(BuildInfo.class)); diff --git a/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java b/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java index 8877ce9bda3cc0e051e82f1432be5f8eafa7df36..cfdd4ee6d739976c8ef678ef0ca8c09470575669 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java @@ -6,6 +6,7 @@ import dev.fitko.fitconnect.api.config.ApplicationConfig; import dev.fitko.fitconnect.api.config.BuildInfo; import dev.fitko.fitconnect.api.config.SchemaConfig; import dev.fitko.fitconnect.api.config.SubscriberConfig; +import dev.fitko.fitconnect.api.domain.schema.SchemaResources; import dev.fitko.fitconnect.api.exceptions.InitializationException; import dev.fitko.fitconnect.api.exceptions.InvalidKeyException; import dev.fitko.fitconnect.api.services.Sender; @@ -13,13 +14,15 @@ import dev.fitko.fitconnect.api.services.Subscriber; import dev.fitko.fitconnect.api.services.auth.OAuthService; import dev.fitko.fitconnect.api.services.crypto.CryptoService; import dev.fitko.fitconnect.api.services.crypto.MessageDigestService; -import dev.fitko.fitconnect.api.services.keys.KeyService; import dev.fitko.fitconnect.api.services.events.EventLogService; import dev.fitko.fitconnect.api.services.events.EventLogVerificationService; import dev.fitko.fitconnect.api.services.events.SecurityEventService; +import dev.fitko.fitconnect.api.services.keys.KeyService; +import dev.fitko.fitconnect.api.services.routing.RoutingService; import dev.fitko.fitconnect.api.services.schema.SchemaProvider; import dev.fitko.fitconnect.api.services.submission.SubmissionService; import dev.fitko.fitconnect.api.services.validation.ValidationService; +import dev.fitko.fitconnect.client.RoutingClient; import dev.fitko.fitconnect.client.SenderClient; import dev.fitko.fitconnect.client.SubscriberClient; import dev.fitko.fitconnect.core.SubmissionSender; @@ -27,11 +30,13 @@ import dev.fitko.fitconnect.core.SubmissionSubscriber; import dev.fitko.fitconnect.core.auth.DefaultOAuthService; import dev.fitko.fitconnect.core.crypto.HashService; import dev.fitko.fitconnect.core.crypto.JWECryptoService; -import dev.fitko.fitconnect.core.keys.PublicKeyService; import dev.fitko.fitconnect.core.events.EventLogApiService; import dev.fitko.fitconnect.core.events.EventLogVerifier; import dev.fitko.fitconnect.core.events.SecurityEventTokenService; -import dev.fitko.fitconnect.core.http.ProxyConfig; +import dev.fitko.fitconnect.core.http.RestService; +import dev.fitko.fitconnect.core.keys.PublicKeyService; +import dev.fitko.fitconnect.core.routing.RouteVerifier; +import dev.fitko.fitconnect.core.routing.RoutingApiService; import dev.fitko.fitconnect.core.schema.SchemaResourceProvider; import dev.fitko.fitconnect.core.submission.SubmissionApiService; import dev.fitko.fitconnect.core.validation.DefaultValidationService; @@ -46,7 +51,7 @@ import java.text.ParseException; import java.util.List; /** - * Factory that constructs clients for {@link Sender} and {@link Subscriber}. + * Factory that constructs {@link SenderClient}, {@link SubscriberClient} and {@link RoutingClient}. */ public final class ClientFactory { @@ -54,13 +59,15 @@ public final class ClientFactory { private static final String CONFIG_ENV_KEY_NAME = "FIT_CONNECT_CONFIG"; private static final String SET_SCHEMA_DIR = "/set-schema"; + + private static final String DESTINATION_SCHEMA_DIR = "/destination-schema"; private static final String METADATA_SCHEMA_DIR = "/metadata-schema"; private ClientFactory() { } /** - * Create a new {@link SenderClient} that is automatically configured via the config.yml file. + * Create a new {@link SenderClient} to send submissions that is automatically configured via the config.yml file. * * @return the sender client */ @@ -69,7 +76,7 @@ public final class ClientFactory { } /** - * Create a new {@link SenderClient} that is automatically configured via a provided {@link ApplicationConfig}. + * Create a new {@link SenderClient} to send submissions that is automatically configured via a provided {@link ApplicationConfig}. * * @return the sender client */ @@ -80,7 +87,7 @@ public final class ClientFactory { /** - * Create a new {@link SubscriberClient} that is automatically configured via config.yml file. + * Create a new {@link SubscriberClient} to receive submissions that is automatically configured via config.yml file. * * @return the subscriber client */ @@ -89,7 +96,7 @@ public final class ClientFactory { } /** - * Create a new {@link SubscriberClient} that is automatically configured via a provided {@link ApplicationConfig}. + * Create a new {@link SubscriberClient} to receive submissions that is automatically configured via a provided {@link ApplicationConfig}. * * @return the subscriber client */ @@ -105,41 +112,73 @@ public final class ClientFactory { return new SubscriberClient(subscriber, privateKey); } - private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) { + /** + * Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}. + * + * @return the routing client + */ + public static RoutingClient routingClient() { + return routingClient(loadConfig()); + } + + /** + * Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}. + * + * @return the routing client + */ + public static RoutingClient routingClient(final ApplicationConfig config) { + LOGGER.info("Initializing routing client ..."); + final RestTemplate restTemplate = getRestTemplate(config, ApplicationConfigLoader.loadBuildInfo()); + final SchemaProvider schemaProvider = getSchemaProvider(); + final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate); + + final MessageDigestService messageDigestService = getMessageDigestService(); + final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService); + final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService); + final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator); + + final RouteVerifier routeVerifier = getRouteVerifier(keyService, validator); + final RoutingService routingService = getRoutingService(config, restTemplate); + + return new RoutingClient(routingService, routeVerifier); + } + + private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) { final RestTemplate restTemplate = getRestTemplate(config, buildInfo); final SchemaProvider schemaProvider = getSchemaProvider(); final MessageDigestService messageDigestService = getMessageDigestService(); final CryptoService cryptoService = getCryptoService(messageDigestService); final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService); - final OAuthService authService = getSubscriberConfiguredAuthService(config, restTemplate); + final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate); final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService); final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator); final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator); final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService); - final SecurityEventService setService = getSecurityEventTokenService(config, validator); - return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService); + return new SubmissionSender(submissionService, eventLogService, cryptoService, validator, keyService); } - private static Sender getSender(final ApplicationConfig config, final BuildInfo buildInfo) { + private static Subscriber getSubscriber(final ApplicationConfig config, final BuildInfo buildInfo) { final RestTemplate restTemplate = getRestTemplate(config, buildInfo); final SchemaProvider schemaProvider = getSchemaProvider(); final MessageDigestService messageDigestService = getMessageDigestService(); final CryptoService cryptoService = getCryptoService(messageDigestService); final ValidationService validator = getValidationService(config, schemaProvider, messageDigestService); - final OAuthService authService = getSenderConfiguredAuthService(config, restTemplate); + final OAuthService authService = getSubscriberConfiguredAuthService(config, restTemplate); final SubmissionService submissionService = getSubmissionService(config, restTemplate, authService); final KeyService keyService = getKeyService(config, restTemplate, authService, submissionService, validator); final EventLogVerificationService eventLogVerifier = getEventLogVerifier(keyService, validator); final EventLogService eventLogService = getEventLogService(config, restTemplate, eventLogVerifier, authService); + final SecurityEventService setService = getSecurityEventTokenService(config, validator); - return new SubmissionSender(submissionService, eventLogService, cryptoService, validator, keyService); + return new SubmissionSubscriber(submissionService, eventLogService, cryptoService, validator, setService); } + private static OAuthService getSenderConfiguredAuthService(final ApplicationConfig config, final RestTemplate restTemplate) { final String clientId = config.getSenderConfig().getClientId(); final String clientSecret = config.getSenderConfig().getClientSecret(); @@ -169,8 +208,8 @@ public final class ClientFactory { } private static RestTemplate getRestTemplate(final ApplicationConfig config, final BuildInfo buildInfo) { - final ProxyConfig proxyConfig = new ProxyConfig(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo); - return proxyConfig.proxyRestTemplate(); + final RestService restService = new RestService(config.getHttpProxyHost(), config.getHttpProxyPort(), buildInfo); + return restService.getRestTemplate(); } private static MessageDigestService getMessageDigestService() { @@ -193,9 +232,19 @@ public final class ClientFactory { } private static SchemaProvider getSchemaProvider() { - final List<String> metadataSchemaFiles = SchemaConfig.getMetadataSchemaFileNames(METADATA_SCHEMA_DIR); final List<String> setSchemaFiles = SchemaConfig.getSetSchemaFilePaths(SET_SCHEMA_DIR); - return new SchemaResourceProvider(setSchemaFiles, metadataSchemaFiles); + final List<String> metadataSchemaFiles = SchemaConfig.getMetadataSchemaFileNames(METADATA_SCHEMA_DIR); + final List<String> destinationSchemaFiles = SchemaConfig.getDestinationSchemaPaths(DESTINATION_SCHEMA_DIR); + final SchemaResources schemaResources = new SchemaResources(setSchemaFiles, metadataSchemaFiles, destinationSchemaFiles); + return new SchemaResourceProvider(schemaResources); + } + + private static RoutingService getRoutingService(final ApplicationConfig config, final RestTemplate restTemplate) { + return new RoutingApiService(config, restTemplate); + } + + private static RouteVerifier getRouteVerifier(final KeyService keyService, final ValidationService validationService) { + return new RouteVerifier(keyService, validationService); } private static RSAKey readRSAKeyFromString(final String key) { diff --git a/client/src/main/java/dev/fitko/fitconnect/client/router/DestinationSearch.java b/client/src/main/java/dev/fitko/fitconnect/client/router/DestinationSearch.java new file mode 100644 index 0000000000000000000000000000000000000000..fc9e37521e8a93ea0d7653291792988cb1b36818 --- /dev/null +++ b/client/src/main/java/dev/fitko/fitconnect/client/router/DestinationSearch.java @@ -0,0 +1,146 @@ +package dev.fitko.fitconnect.client.router; + +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.util.regex.Pattern; + + +/** + * Builds a new search request for a service identifier and AT MAX. ONE OTHER search criterion (ars | ags | areaId). + */ +@Value +@AllArgsConstructor +public class DestinationSearch { + + String leikaKey; + String ars; + String ags; + String areaId; + + int offset; + int limit; + + public static Builder Builder() { + return new Builder(); + } + + public static class Builder { + + private static final Pattern AGS_PATTERN = Pattern.compile("^(\\d{2}|\\d{3}|\\d{5}|\\d{8})$"); + private static final Pattern ARS_PATTERN = Pattern.compile("^(\\d{2}|\\d{3}|\\d{5}|\\d{9}|\\d{12})$"); + private static final Pattern AREA_ID_PATTERN = Pattern.compile("^\\d{1,}"); + private static final Pattern LEIKA_KEY_PATTERN = Pattern.compile("^99\\d{12}$"); + + private String leikaKey; + private String ars; + private String ags; + private String areaId; + private int offset = 0; + private int limit = 100; + + /** + * Leika Key of the requested service. + * + * @param leikaKey service identifier + * @return Builder + * @throws IllegalArgumentException if the leika key pattern is not matching + */ + public Builder withLeikaKey(final String leikaKey) throws IllegalArgumentException { + if (!LEIKA_KEY_PATTERN.matcher(leikaKey).matches()) { + throw new IllegalArgumentException("Leika key does not match allowed pattern " + LEIKA_KEY_PATTERN); + } + this.leikaKey = leikaKey; + return this; + } + + /** + * Official municipal code of the place. + * + * @param ars amtlicher regionalschlüssel + * @return Builder + * @throws IllegalArgumentException if the ars key pattern is not matching + */ + public Builder withArs(final String ars) throws IllegalArgumentException{ + if (!ARS_PATTERN.matcher(ars).matches()) { + throw new IllegalArgumentException("ARS key does not match allowed pattern " + ARS_PATTERN); + } + this.ars = ars; + + return this; + } + + /** + * Official regional key of the area. + * + * @param ags amtlicher gemeindeschlüssel + * @return Builder + * @throws IllegalArgumentException if the ags key pattern is not matching + */ + public Builder withAgs(final String ags) throws IllegalArgumentException { + if (!AGS_PATTERN.matcher(ags).matches()) { + throw new IllegalArgumentException("AGS key does not match allowed pattern " + AGS_PATTERN); + } + this.ags = ags; + return this; + } + + /** + * ID of the area. This ID can be determined via the routing clients <code>findAreas</code> search. + * + * @param areaId id of the area + * @return Builder + * @see dev.fitko.fitconnect.client.RoutingClient#findAreas(String, int, int) + * @throws IllegalArgumentException if the area id pattern is not matching + */ + public Builder withAreaId(final String areaId) throws IllegalArgumentException { + if (!AREA_ID_PATTERN.matcher(areaId).matches()) { + throw new IllegalArgumentException("AreaId key does not match allowed pattern " + AREA_ID_PATTERN); + } + this.areaId = areaId; + return this; + } + + /** + * Start position of the subset of the result set. Default is 0. + * + * @param offset start of the subset + * @return Builder + * @throws IllegalArgumentException if the offset is a negative number + */ + public Builder withOffset(final int offset) throws IllegalArgumentException{ + if (limit < 0) { + throw new IllegalArgumentException("offset must be positive"); + } + this.offset = offset; + return this; + } + + /** + * Max. size of the subset of the result set. Maximum is 500. Default is 100. + * + * @param limit max. entries in the subset + * @return Builder + * @throws IllegalArgumentException if the limit is > 500 + */ + public Builder withLimit(final int limit) throws IllegalArgumentException { + if (limit > 500) { + throw new IllegalArgumentException("limit must no be > 500"); + } + this.limit = limit; + return this; + } + + /** + * Construct the search request. + * + * @return DestinationSearch + */ + public DestinationSearch build() throws IllegalArgumentException { + if(leikaKey == null){ + throw new IllegalArgumentException("leikaKey is mandatory"); + } + return new DestinationSearch(leikaKey, ars, ags, areaId, offset, limit); + } + } +} diff --git a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java index 00d872b95886b477b8ea98c063821e2eba4c7d66..b1dec39424d25c1c48a554f60c66a3c7df176caa 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendEncryptedSubmissionStrategy.java @@ -2,6 +2,7 @@ package dev.fitko.fitconnect.client.sender.strategies; import dev.fitko.fitconnect.api.domain.model.submission.CreateSubmission; import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission; +import dev.fitko.fitconnect.api.domain.model.submission.ServiceType; import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; import dev.fitko.fitconnect.api.exceptions.RestApiException; @@ -14,12 +15,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; +import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildCreateSubmission; import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSentSubmission; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmissionToAnnounce; import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmitSubmission; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.toAttachmentPayloads; public class SendEncryptedSubmissionStrategy { @@ -34,9 +36,9 @@ public class SendEncryptedSubmissionStrategy { public SentSubmission send(final EncryptedSubmissionPayload submissionPayload) { try { - final UUID submissionId = announceNewSubmission(submissionPayload); - final SubmitSubmission submitSubmission = buildSubmitSubmission(submissionId, submissionPayload); final List<AttachmentPayload> attachmentPayloads = toAttachmentPayloads(submissionPayload.getEncryptedAttachments()); + final UUID submissionId = announceNewSubmission(submissionPayload, attachmentPayloads); + final SubmitSubmission submitSubmission = buildSubmitSubmission(submissionId, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedMetadata()); final var startTimeAttachmentUpload = StopWatch.start(); uploadAttachments(attachmentPayloads, submissionId); @@ -47,7 +49,7 @@ public class SendEncryptedSubmissionStrategy { LOGGER.info("Uploading submission took {}", StopWatch.stopWithFormattedTime(startTimeSubmissionUpload)); LOGGER.info("SUCCESSFULLY HANDED IN SUBMISSION !"); - return buildSentSubmission(submissionPayload, attachmentPayloads, submission); + return buildSentSubmission(attachmentPayloads, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedData(), submission); } catch (final RestApiException e) { LOGGER.error("Sending submission failed", e); @@ -58,8 +60,10 @@ public class SendEncryptedSubmissionStrategy { return null; } - private UUID announceNewSubmission(final EncryptedSubmissionPayload submissionPayload) { - final CreateSubmission submissionToAnnounce = buildSubmissionToAnnounce(submissionPayload); + private UUID announceNewSubmission(final EncryptedSubmissionPayload submissionPayload, final List<AttachmentPayload> attachmentPayloads) { + final UUID destinationId = submissionPayload.getDestinationId(); + final ServiceType serviceType = submissionPayload.getServiceType(); + final CreateSubmission submissionToAnnounce = buildCreateSubmission(destinationId, attachmentPayloads, serviceType); return sender.createSubmission(submissionToAnnounce).getSubmissionId(); } @@ -71,4 +75,14 @@ public class SendEncryptedSubmissionStrategy { attachmentPayloads.forEach(a -> sender.uploadAttachment(submissionId, a.getAttachmentId(), a.getEncryptedData())); } } + + private List<AttachmentPayload> toAttachmentPayloads(final Map<UUID, String> encryptedAttachments) { + return encryptedAttachments.entrySet() + .stream() + .map(attachment -> AttachmentPayload.builder() + .encryptedData(attachment.getValue()) + .attachmentId(attachment.getKey()) + .build()) + .collect(Collectors.toList()); + } } diff --git a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java index 9df636fb65fd6f633d57ab953170ac0ff3cd56ae..162fef4271a78cb7cd522aa03fb5a26b655a245d 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/sender/strategies/SendNewSubmissionStrategy.java @@ -2,7 +2,14 @@ package dev.fitko.fitconnect.client.sender.strategies; import com.nimbusds.jose.jwk.RSAKey; import dev.fitko.fitconnect.api.domain.model.destination.Destination; +import dev.fitko.fitconnect.api.domain.model.destination.DestinationService; +import dev.fitko.fitconnect.api.domain.model.metadata.ContentStructure; +import dev.fitko.fitconnect.api.domain.model.metadata.Hash; import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; +import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType; +import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose; import dev.fitko.fitconnect.api.domain.model.metadata.data.Data; import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType; import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; @@ -34,20 +41,18 @@ import java.net.URLConnection; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Collection; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.UUID; + import java.util.stream.Collectors; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildMetadataToSend; +import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0; +import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildCreateSubmission; import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSentSubmission; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmissionToAnnounce; import static dev.fitko.fitconnect.client.util.SubmissionUtil.buildSubmitSubmission; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.createData; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.getFileAttachmentPayload; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.getSchemaUriForMimeType; -import static dev.fitko.fitconnect.client.util.SubmissionUtil.getSubmissionSchemasFromDestination; + public class SendNewSubmissionStrategy { @@ -70,7 +75,7 @@ public class SendNewSubmissionStrategy { final Destination destination = sender.getDestination(destinationId); final List<AttachmentPayload> encryptedAttachments = encryptAndHashAttachments(encryptionKey, submissionPayload.getAttachments()); - final CreateSubmission newSubmission = buildSubmissionToAnnounce(destinationId, serviceType, encryptedAttachments); + final CreateSubmission newSubmission = buildCreateSubmission(destinationId, encryptedAttachments, serviceType); final SubmissionForPickup announcedSubmission = sender.createSubmission(newSubmission); final UUID announcedSubmissionId = announcedSubmission.getSubmissionId(); @@ -119,20 +124,74 @@ public class SendNewSubmissionStrategy { private Metadata buildMetadata(final SubmissionPayload submissionPayload, final Destination destination, final List<AttachmentPayload> encryptedAttachments) { final String hashedData = sender.createHash(submissionPayload.getData().getBytes(StandardCharsets.UTF_8)); - final URI schemaUri = getSchemaUri(submissionPayload.getDataMimeType(), destination); - final Data data = createData(submissionPayload, hashedData, schemaUri); - return buildMetadataToSend(data, submissionPayload.getServiceType(), encryptedAttachments); + + final MimeType dataMimeType = submissionPayload.getDataMimeType(); + final URI schemaUri = getSubmissionSchemaFromDestination(destination, dataMimeType); + final Data data = createData(dataMimeType, hashedData, schemaUri); + final PublicServiceType publicServiceType = buildPublicServiceType(submissionPayload.getServiceType()); + final List<Attachment> attachmentMetadata = encryptedAttachments.stream().map(this::toHashedAttachment).collect(Collectors.toList()); + + final var contentStructure = new ContentStructure(); + contentStructure.setAttachments(attachmentMetadata); + contentStructure.setData(data); + + final var metadata = new Metadata(); + metadata.setSchema(METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + + return metadata; + } + + private Data createData(final MimeType mimeType, final String hashedData, final URI schemaUri) { + final var hash = new Hash(); + hash.setContent(hashedData); + hash.setSignatureType(SignatureType.SHA_512); + + final var submissionSchema = new SubmissionSchema(); + submissionSchema.setMimeType(mimeType); + submissionSchema.setSchemaUri(schemaUri); + + final var data = new Data(); + data.setSubmissionSchema(submissionSchema); + data.setHash(hash); + return data; } - private URI getSchemaUri(final MimeType mimeType, final Destination destination) { - final List<SubmissionSchema> submissionSchemas = getSubmissionSchemasFromDestination(destination); - final String supportedSchemas = submissionSchemas.stream().map(s -> s.getMimeType().toString()).collect(Collectors.joining(" | ")); - final Optional<URI> schemaUriForMimeType = getSchemaUriForMimeType(submissionSchemas, mimeType); - if (schemaUriForMimeType.isEmpty()) { - LOGGER.error("Destination supports the mime-type '{}'. Mime-type {} is not allowed, please check the destination", supportedSchemas, mimeType); - throw new SchemaNotFoundException("Schema for mime-type " + mimeType + " not found"); + private PublicServiceType buildPublicServiceType(final ServiceType serviceType) { + final var publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier(serviceType.getIdentifier()); + if (serviceType.getName() != null) { + publicServiceType.setName(serviceType.getName()); } - return schemaUriForMimeType.get(); + if (serviceType.getDescription() != null) { + publicServiceType.setDescription(serviceType.getDescription()); + } + return publicServiceType; + } + + private Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) { + final var attachment = new Attachment(); + attachment.setAttachmentId(attachmentPayload.getAttachmentId()); + attachment.setPurpose(Purpose.ATTACHMENT); + attachment.setFilename(attachmentPayload.getFile().getName()); + attachment.setMimeType(attachmentPayload.getMimeType()); + + final var hash = new Hash(); + hash.setContent(attachmentPayload.getHashedData()); + hash.setSignatureType(SignatureType.SHA_512); + attachment.setHash(hash); + return attachment; + } + + private URI getSubmissionSchemaFromDestination(final Destination destination, final MimeType mimeType) { + return destination.getServices().stream() + .map(DestinationService::getSubmissionSchemas) + .flatMap(Collection::stream) + .filter(schema -> schema.getMimeType().equals(mimeType)) + .map(SubmissionSchema::getSchemaUri) + .findFirst() + .orElseThrow(() -> new SchemaNotFoundException("Destination does not support data with mime-type " + mimeType)); } private void uploadAttachments(final List<AttachmentPayload> attachmentPayloads, final UUID submissionId) { @@ -146,7 +205,7 @@ public class SendNewSubmissionStrategy { private List<AttachmentPayload> encryptAndHashAttachments(final RSAKey encryptionKey, final List<File> attachments) { return attachments.stream() - .map(getFileAttachmentPayload()) + .map(file -> AttachmentPayload.builder().file(file).attachmentId(UUID.randomUUID()).build()) .filter(Objects::nonNull) .map(payload -> encryptAndHashAttachment(encryptionKey, payload)) .collect(Collectors.toList()); @@ -160,12 +219,13 @@ public class SendNewSubmissionStrategy { final String hashedBytes = sender.createHash(rawData); return attachmentPayload.withEncryptedData(encryptedAttachment) .withHashedData(hashedBytes) - .withMimeType(setMimeType(attachmentPayload)); + .withMimeType(detectMimeTypeFromFile(attachmentPayload)); } catch (final Exception e) { throw new AttachmentCreationException("Attachment '" + file.getAbsolutePath() + "' could not be created ", e); } } - private String setMimeType(final AttachmentPayload attachmentPayload) { + + private String detectMimeTypeFromFile(final AttachmentPayload attachmentPayload) { final File file = attachmentPayload.getFile(); String mimeType; try { diff --git a/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java b/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java index 45acaa70fa6dd44207bb419e18a67169d5a4212b..da952802fbdf83007896c337b2f52538fe4e2036 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/util/SubmissionUtil.java @@ -1,88 +1,36 @@ package dev.fitko.fitconnect.client.util; import com.nimbusds.jose.JWEObject; -import dev.fitko.fitconnect.api.domain.model.destination.Destination; -import dev.fitko.fitconnect.api.domain.model.destination.DestinationService; import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; -import dev.fitko.fitconnect.api.domain.model.metadata.*; -import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; -import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose; -import dev.fitko.fitconnect.api.domain.model.metadata.data.Data; -import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType; -import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; -import dev.fitko.fitconnect.api.domain.model.submission.*; +import dev.fitko.fitconnect.api.domain.model.submission.CreateSubmission; +import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission; +import dev.fitko.fitconnect.api.domain.model.submission.ServiceType; +import dev.fitko.fitconnect.api.domain.model.submission.Submission; +import dev.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; import dev.fitko.fitconnect.client.sender.model.AttachmentPayload; -import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload; -import dev.fitko.fitconnect.client.sender.model.SubmissionPayload; -import dev.fitko.fitconnect.client.subscriber.model.DecryptedAttachmentPayload; -import dev.fitko.fitconnect.client.subscriber.model.ReceivedAttachment; -import java.io.File; -import java.net.URI; import java.text.ParseException; -import java.util.*; -import java.util.function.Function; +import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; -import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0; - +/** + * Helper to create types for announced, sent and submit submissions. + */ public final class SubmissionUtil { private SubmissionUtil() { } - private static CreateSubmission createSubmission(final UUID destinationId, final ServiceType serviceType, final List<UUID> attachmentIdsToAnnounce) { + public static CreateSubmission buildCreateSubmission(final UUID destinationId, final List<AttachmentPayload> attachments, final ServiceType serviceType) { return CreateSubmission.builder() .destinationId(destinationId) - .announcedAttachments(attachmentIdsToAnnounce) + .announcedAttachments(attachments.stream().map(AttachmentPayload::getAttachmentId).collect(Collectors.toList())) .serviceType(serviceType) .build(); } - public static PublicServiceType buildPublicServiceType(final ServiceType serviceType) { - final var publicServiceType = new PublicServiceType(); - publicServiceType.setIdentifier(serviceType.getIdentifier()); - if (serviceType.getName() != null) { - publicServiceType.setName(serviceType.getName()); - } - if (serviceType.getDescription() != null) { - publicServiceType.setDescription(serviceType.getDescription()); - } - return publicServiceType; - } - - public static Metadata buildMetadataToSend(final Data data, final ServiceType serviceType, final List<AttachmentPayload> encryptedAttachments) { - final PublicServiceType publicServiceType = buildPublicServiceType(serviceType); - final List<Attachment> attachmentMetadata = toAttachmentMetadata(encryptedAttachments); - return buildMetadata(attachmentMetadata, data, publicServiceType); - } - - public static Metadata buildMetadata(final List<Attachment> attachments, final Data data, final PublicServiceType publicServiceType) { - final var contentStructure = new ContentStructure(); - contentStructure.setAttachments(attachments); - contentStructure.setData(data); - - final var metadata = new Metadata(); - metadata.setSchema(METADATA_V_1_0_0.toString()); - metadata.setContentStructure(contentStructure); - metadata.setPublicServiceType(publicServiceType); - return metadata; - } - - public static CreateSubmission buildSubmissionToAnnounce(final EncryptedSubmissionPayload submissionPayload) { - final ServiceType serviceType = submissionPayload.getServiceType(); - final List<UUID> attachmentIds = new ArrayList<>(submissionPayload.getEncryptedAttachments().keySet()); - return createSubmission(submissionPayload.getDestinationId(), serviceType, attachmentIds); - } - - public static SubmitSubmission buildSubmitSubmission(final UUID submissionId, final EncryptedSubmissionPayload submissionPayload) { - final SubmitSubmission submission = new SubmitSubmission(); - submission.setSubmissionId(submissionId); - submission.setEncryptedData(submissionPayload.getEncryptedData()); - submission.setEncryptedMetadata(submissionPayload.getEncryptedMetadata()); - return submission; - } - public static SubmitSubmission buildSubmitSubmission(final UUID submissionId, final String encryptedData, final String encryptedMetadata) { final SubmitSubmission submission = new SubmitSubmission(); submission.setSubmissionId(submissionId); @@ -91,83 +39,6 @@ public final class SubmissionUtil { return submission; } - private static Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) { - final var attachment = new Attachment(); - attachment.setAttachmentId(attachmentPayload.getAttachmentId()); - attachment.setPurpose(Purpose.ATTACHMENT); - attachment.setFilename(attachmentPayload.getFile().getName()); - attachment.setMimeType(attachmentPayload.getMimeType()); - - final var hash = new Hash(); - hash.setContent(attachmentPayload.getHashedData()); - hash.setSignatureType(SignatureType.SHA_512); - attachment.setHash(hash); - return attachment; - } - - private static List<UUID> toAttachmentIds(final List<AttachmentPayload> attachmentPayloads) { - return attachmentPayloads.stream() - .map(AttachmentPayload::getAttachmentId) - .collect(Collectors.toList()); - } - - public static List<Attachment> toAttachmentMetadata(final List<AttachmentPayload> attachmentPayloads) { - return attachmentPayloads.stream() - .map(SubmissionUtil::toHashedAttachment) - .collect(Collectors.toList()); - } - - private static Function<Map.Entry<UUID, String>, AttachmentPayload> getEncryptedAttachmentPayload() { - return attachment -> AttachmentPayload.builder() - .encryptedData(attachment.getValue()) - .attachmentId(attachment.getKey()) - .build(); - } - - public static Function<File, AttachmentPayload> getFileAttachmentPayload() { - return file -> AttachmentPayload.builder() - .file(file) - .attachmentId(UUID.randomUUID()) - .build(); - } - - public static List<AttachmentPayload> toAttachmentPayloads(final Map<UUID, String> encryptedAttachments) { - return encryptedAttachments.entrySet() - .stream() - .map(getEncryptedAttachmentPayload()) - .collect(Collectors.toList()); - } - - public static CreateSubmission buildSubmissionToAnnounce(final UUID destinationId, final ServiceType serviceType, final List<AttachmentPayload> encryptedAttachments) { - final List<UUID> attachmentIdsToAnnounce = toAttachmentIds(encryptedAttachments); - return createSubmission(destinationId, serviceType, attachmentIdsToAnnounce); - } - - public static List<SubmissionSchema> getSubmissionSchemasFromDestination(final Destination destination) { - return destination.getServices().stream() - .map(DestinationService::getSubmissionSchemas) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - } - - public static Optional<URI> getSchemaUriForMimeType(final List<SubmissionSchema> submissionSchemas, final MimeType mimeType) { - return submissionSchemas.stream() - .filter(schema -> schema.getMimeType().equals(mimeType)) - .map(SubmissionSchema::getSchemaUri) - .findFirst(); - } - - public static DecryptedAttachmentPayload buildDecryptedAttachmentPayload(final Attachment attachmentMetadata, final byte[] decryptedAttachment) { - return DecryptedAttachmentPayload.builder() - .decryptedContent(decryptedAttachment) - .attachmentMetadata(attachmentMetadata) - .build(); - } - - public static SentSubmission buildSentSubmission(final EncryptedSubmissionPayload submissionPayload, final List<AttachmentPayload> encryptedAttachments, final Submission submission) { - return buildSentSubmission(encryptedAttachments, submissionPayload.getEncryptedData(), submissionPayload.getEncryptedMetadata(), submission); - } - public static SentSubmission buildSentSubmission(final List<AttachmentPayload> encryptedAttachments, final String encryptedData, final String encryptedMetadata, final Submission submission) { final AuthenticationTags authenticationTags = new AuthenticationTags(); @@ -183,45 +54,6 @@ public final class SubmissionUtil { .build(); } - public static List<ReceivedAttachment> mapToReceivedAttachments(final List<DecryptedAttachmentPayload> decryptedAttachmentPayloads) { - return decryptedAttachmentPayloads.stream() - .map(SubmissionUtil::mapToReceivedAttachment) - .collect(Collectors.toList()); - } - - public static Data createData(final SubmissionPayload submissionPayload, final String hashedData, final URI schemaUri) { - final var hash = new Hash(); - hash.setContent(hashedData); - hash.setSignatureType(SignatureType.SHA_512); - - final var submissionSchema = new SubmissionSchema(); - submissionSchema.setMimeType(submissionPayload.getDataMimeType()); - submissionSchema.setSchemaUri(schemaUri); - - final var data = new Data(); - data.setSubmissionSchema(submissionSchema); - data.setHash(hash); - return data; - } - - public static String getDataHashFromMetadata(final Metadata metadata) { - return metadata.getContentStructure() - .getData() - .getHash() - .getContent(); - } - - private static ReceivedAttachment mapToReceivedAttachment(final DecryptedAttachmentPayload payload) { - final Attachment metadata = payload.getAttachmentMetadata(); - return ReceivedAttachment.builder() - .attachmentId(metadata.getAttachmentId()) - .filename(metadata.getFilename()) - .mimeType(metadata.getMimeType()) - .description(metadata.getDescription()) - .data(payload.getDecryptedContent()) - .build(); - } - private static Map<UUID, String> getAuthTagsFromAttachments(final List<AttachmentPayload> encryptedAttachments) { return encryptedAttachments.stream().collect(Collectors.toMap(AttachmentPayload::getAttachmentId, SubmissionUtil::getAuthTag)); } @@ -237,5 +69,4 @@ public final class SubmissionUtil { return ""; } } - } diff --git a/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java b/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java index b714248a674ef1fcf8772dceb74f14f3ba267013..de8fb13f7198c2374c1633a9a06e0c43755f991f 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/util/ValidDataGuard.java @@ -1,20 +1,19 @@ package dev.fitko.fitconnect.client.util; +import dev.fitko.fitconnect.api.domain.model.destination.DestinationService; import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType; import dev.fitko.fitconnect.api.domain.model.submission.ServiceType; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; import dev.fitko.fitconnect.api.services.Sender; import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload; import dev.fitko.fitconnect.client.sender.model.SubmissionPayload; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.Objects; import java.util.UUID; import java.util.regex.Pattern; public class ValidDataGuard { - private static final Logger LOGGER = LoggerFactory.getLogger(ValidDataGuard.class); private static final Pattern LEIKA_KEY_PATTERN = Pattern.compile("^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$"); private final Sender sender; @@ -22,57 +21,87 @@ public class ValidDataGuard { this.sender = sender; } + /** + * Checks if the encrypted submission payload is in a valid state for sending. + * + * @param encryptedSubmissionPayload payload to be checked + * @throws IllegalArgumentException if one of the checks fails + * @throws IllegalStateException if the destination does not have a declared service type + */ public void ensureValidDataPayload(final EncryptedSubmissionPayload encryptedSubmissionPayload) { if (encryptedSubmissionPayload == null) { throw new IllegalArgumentException("Encrypted payload must not be null."); } + if (encryptedSubmissionPayload.getEncryptedData() == null) { + throw new IllegalArgumentException("Encrypted data is mandatory, but was null."); + } if (encryptedSubmissionPayload.getEncryptedMetadata() == null) { throw new IllegalArgumentException("Encrypted metadata must not be null."); } - testDefaults(encryptedSubmissionPayload.getDestinationId(), encryptedSubmissionPayload.getEncryptedData(), encryptedSubmissionPayload.getServiceType()); + testDefaults(encryptedSubmissionPayload.getDestinationId(), encryptedSubmissionPayload.getServiceType()); } + /** + * Checks if the unencrypted submission payload is in a valid state for sending. + * + * @param submissionPayload payload to be checked + * @throws IllegalArgumentException if one of the checks fails + */ public void ensureValidDataPayload(final SubmissionPayload submissionPayload) { if (submissionPayload == null) { throw new IllegalArgumentException("Payload must not be null."); } - if (isDataFormatInvalid(submissionPayload)) { - throw new IllegalArgumentException("Data format is invalid, please provide well-formed data."); + if (submissionPayload.getData() == null) { + throw new IllegalArgumentException("Data is mandatory, but was null."); } - testDefaults(submissionPayload.getDestinationId(), submissionPayload.getData(), submissionPayload.getServiceType()); + testOnValidDataFormat(submissionPayload); + testDefaults(submissionPayload.getDestinationId(), submissionPayload.getServiceType()); } - public void testDefaults(final UUID destinationId, final String data, final ServiceType serviceType) { + private void testDefaults(final UUID destinationId, final ServiceType serviceType) { if (destinationId == null) { throw new IllegalArgumentException("DestinationId is mandatory, but was null."); - } else if (data == null) { - throw new IllegalArgumentException("Data is mandatory, but was null."); } else if (serviceType == null) { throw new IllegalArgumentException("ServiceType is mandatory, but was null."); } else if (serviceType.getIdentifier() == null) { throw new IllegalArgumentException("Leika key is mandatory, but was null."); } else if (noValidLeikaKeyPattern(serviceType.getIdentifier())) { throw new IllegalArgumentException("LeikaKey has invalid format, please follow: ^urn:[a-z0-9][a-z0-9-]{0,31}:[a-z0-9()+,.:=@;$_!*'%/?#-]+$."); + } else if (serviceTypeDoesNotMatchDestination(destinationId, serviceType)) { + throw new IllegalArgumentException("Provided service type '" + serviceType.getIdentifier() + "' is not allowed by the destination "); } } - private boolean isDataFormatInvalid(final SubmissionPayload submissionPayload) { + private boolean serviceTypeDoesNotMatchDestination(final UUID destinationId, final ServiceType serviceType) { + return sender.getDestination(destinationId).getServices().stream() + .map(DestinationService::getIdentifier) + .filter(Objects::nonNull) + .filter(serviceIdentifier -> serviceIdentifier.equals(serviceType.getIdentifier())) + .findFirst() + .isEmpty(); + } + private void testOnValidDataFormat(final SubmissionPayload submissionPayload) { final MimeType dataMimeType = submissionPayload.getDataMimeType(); if (dataMimeType.equals(MimeType.APPLICATION_JSON)) { - final ValidationResult validationResult = sender.validateJsonFormat(submissionPayload.getData()); - if (validationResult.hasError()) { - LOGGER.error("Data is not in expected json format, please provide valid json {}", validationResult.getError().getMessage()); - return true; - } + checkJsonFormat(submissionPayload); } else if (dataMimeType.equals(MimeType.APPLICATION_XML)) { - final ValidationResult validationResult = sender.validateXmlFormat(submissionPayload.getData()); - if (validationResult.hasError()) { - LOGGER.error("Data is not in expected xml format, please provide valid xml {}", validationResult.getError().getMessage()); - return true; - } + checkXmlFormat(submissionPayload); + } + } + + private void checkXmlFormat(final SubmissionPayload submissionPayload) { + final ValidationResult validationResult = sender.validateXmlFormat(submissionPayload.getData()); + if (validationResult.hasError()) { + throw new IllegalArgumentException("Data is not in expected xml format, please provide valid xml: " + validationResult.getError().getMessage()); + } + } + + private void checkJsonFormat(final SubmissionPayload submissionPayload) { + final ValidationResult validationResult = sender.validateJsonFormat(submissionPayload.getData()); + if (validationResult.hasError()) { + throw new IllegalArgumentException("Data is not in expected json format, please provide valid json: " + validationResult.getError().getMessage()); } - return false; } private boolean noValidLeikaKeyPattern(final String leikaKey) { diff --git a/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java b/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java index dcdf68c90b4be23a2cf604280601063ad0762867..bd74e818eb4d0418cc64af235d1cd8bf2c8bf035 100644 --- a/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java +++ b/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java @@ -2,37 +2,46 @@ package dev.fitko.fitconnect.client; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.jwk.RSAKey; -import dev.fitko.fitconnect.api.config.*; +import dev.fitko.fitconnect.api.config.ApplicationConfig; +import dev.fitko.fitconnect.api.config.BuildInfo; +import dev.fitko.fitconnect.api.config.Environment; +import dev.fitko.fitconnect.api.config.EnvironmentName; +import dev.fitko.fitconnect.api.config.SenderConfig; +import dev.fitko.fitconnect.api.config.SubscriberConfig; import dev.fitko.fitconnect.api.domain.auth.OAuthToken; import dev.fitko.fitconnect.api.domain.model.event.Event; import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry; import dev.fitko.fitconnect.api.domain.model.event.EventStatus; import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog; +import dev.fitko.fitconnect.api.domain.model.metadata.ContentStructure; import dev.fitko.fitconnect.api.domain.model.metadata.Hash; import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType; import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType; import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose; import dev.fitko.fitconnect.api.domain.model.metadata.data.Data; import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType; import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; -import dev.fitko.fitconnect.api.domain.model.submission.ServiceType; +import dev.fitko.fitconnect.api.domain.model.route.Area; +import dev.fitko.fitconnect.api.domain.model.route.Route; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; +import dev.fitko.fitconnect.api.exceptions.RestApiException; import dev.fitko.fitconnect.api.services.crypto.CryptoService; import dev.fitko.fitconnect.client.factory.ClientFactory; +import dev.fitko.fitconnect.client.router.DestinationSearch; import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder; import dev.fitko.fitconnect.client.sender.SubmissionBuilder; import dev.fitko.fitconnect.client.sender.model.AttachmentPayload; import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission; -import dev.fitko.fitconnect.client.util.SubmissionUtil; import dev.fitko.fitconnect.core.auth.DefaultOAuthService; import dev.fitko.fitconnect.core.crypto.HashService; import dev.fitko.fitconnect.core.crypto.JWECryptoService; +import dev.fitko.fitconnect.core.http.RestService; import org.apache.tika.mime.MimeTypes; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.springframework.web.client.RestTemplate; import java.io.File; import java.io.IOException; @@ -41,23 +50,35 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.text.ParseException; import java.time.Duration; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; +import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.*; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * This test uses real credentials and endpoints stored in gitlab ci variables to test the sdk against a real system */ class ClientIntegrationTest { - private static final String authBaseUrl = "https://auth-testing.fit-connect.fitko.dev"; - private static final String submissionBaseUrl = "https://submission-api-testing.fit-connect.fitko.dev"; - @Nested class SendSubmissionTests { @@ -116,19 +137,19 @@ class ClientIntegrationTest { final String encryptedData = cryptoService.encryptString(encryptionKey, jsonData); final String encryptedAttachment = cryptoService.encryptString(encryptionKey, attachmentData); - final SubmissionSchema submissionSchema = new SubmissionSchema(); + final var submissionSchema = new SubmissionSchema(); submissionSchema.setSchemaUri(URI.create("https://schema.fitko.de/fim/s00000000009_1.0.0.schema.json")); submissionSchema.setMimeType(MimeType.APPLICATION_JSON); - final Hash hash = new Hash(); - hash.setSignatureType(SignatureType.SHA_512); - hash.setContent(cryptoService.hashBytes(jsonData.getBytes(StandardCharsets.UTF_8))); + final var dataHash = new Hash(); + dataHash.setSignatureType(SignatureType.SHA_512); + dataHash.setContent(cryptoService.hashBytes(jsonData.getBytes(StandardCharsets.UTF_8))); - final Data data = new Data(); - data.setHash(hash); + final var data = new Data(); + data.setHash(dataHash); data.setSubmissionSchema(submissionSchema); - final AttachmentPayload attachmentPayload = AttachmentPayload.builder() + final var attachmentPayload = AttachmentPayload.builder() .encryptedData(encryptedAttachment) .file(attachmentFile) .mimeType(MimeTypes.PLAIN_TEXT) @@ -136,14 +157,29 @@ class ClientIntegrationTest { .hashedData(cryptoService.hashBytes(attachmentData.getBytes(StandardCharsets.UTF_8))) .build(); - final ServiceType serviceType = ServiceType.builder() - .name("Test Service") - .identifier("urn:de:fim:leika:leistung:99400048079000") - .build(); + final var publicServiceType = new PublicServiceType(); + publicServiceType.setName("Test Service"); + publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var attachment = new Attachment(); + attachment.setAttachmentId(attachmentPayload.getAttachmentId()); + attachment.setPurpose(Purpose.ATTACHMENT); + attachment.setFilename(attachmentPayload.getFile().getName()); + attachment.setMimeType(attachmentPayload.getMimeType()); + + final var attachmentHash = new Hash(); + attachmentHash.setContent(attachmentPayload.getHashedData()); + attachmentHash.setSignatureType(SignatureType.SHA_512); + attachment.setHash(attachmentHash); + + final var contentStructure = new ContentStructure(); + contentStructure.setAttachments(List.of(attachment)); + contentStructure.setData(data); - final PublicServiceType publicServiceType = SubmissionUtil.buildPublicServiceType(serviceType); - final List<Attachment> attachmentMetadata = SubmissionUtil.toAttachmentMetadata(List.of(attachmentPayload)); - final Metadata metadata = SubmissionUtil.buildMetadata(attachmentMetadata, data, publicServiceType); + final var metadata = new Metadata(); + metadata.setSchema(METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); final String encryptedMetadata = cryptoService.encryptBytes(encryptionKey, new ObjectMapper().writeValueAsBytes(metadata)); @@ -316,7 +352,7 @@ class ClientIntegrationTest { final var submission = SubmissionBuilder.Builder() .withJsonData("{ \"data\": \"Beispiel Fachdaten\" }") .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) - .withServiceType("Test Service", "urn:de:fim:leika:") + .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") .build(); final var sentSubmission = ClientFactory.senderClient(config).submit(submission); @@ -348,7 +384,7 @@ class ClientIntegrationTest { .withAttachment(new File("src/test/resources/attachment.txt")) .withJsonData("{ \"data\": \"Beispiel Fachdaten\" }") .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) - .withServiceType("Test Service", "urn:de:fim:leika:") + .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") .build(); final var sentSubmission = ClientFactory.senderClient(config).submit(submission); @@ -387,7 +423,7 @@ class ClientIntegrationTest { .withAttachment(new File("src/test/resources/attachment.txt")) .withJsonData("{ \"data\": \"Beispiel Fachdaten\" }") .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) - .withServiceType("Test Service", "urn:de:fim:leika:") + .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") .build(); final var sentSubmission = ClientFactory.senderClient(config).submit(submission); @@ -406,6 +442,95 @@ class ClientIntegrationTest { } + @Nested + class RoutingTests { + + @Test + @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*") + void testFindDestinationsWithRegionalKey() { + + // Given + final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true); + + final RoutingClient routingClient = ClientFactory.routingClient(config); + + final DestinationSearch search = DestinationSearch.Builder() + .withLeikaKey("99123456760610") + .withArs("064350014014") + .build(); + + // When + final List<Route> routes = routingClient.findDestinations(search); + + // Then + assertThat(routes, hasSize(1)); + assertThat(routes.get(0).getDestinationId(), is(UUID.fromString("d40e7b13-da98-4b09-9e16-bbd61ca81510"))); + } + + @Test + @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*") + void testFindDestinationsWithAreaId() { + + // Given + final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true); + + final RoutingClient routingClient = ClientFactory.routingClient(config); + + final DestinationSearch search = DestinationSearch.Builder() + .withLeikaKey("99123456760610") + .withAreaId("931") + .build(); + + // When + final List<Route> routes = routingClient.findDestinations(search); + + // Then + assertThat(routes, hasSize(1)); + assertThat(routes.get(0).getDestinationId(), is(UUID.fromString("d40e7b13-da98-4b09-9e16-bbd61ca81510"))); + } + + @Test + @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*") + void testFindDestinationsWithMultipleAreaSearchCriteria() { + + // Given + final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true); + + final RoutingClient routingClient = ClientFactory.routingClient(config); + + final DestinationSearch search = DestinationSearch.Builder() + .withLeikaKey("99123456760610") + .withArs("064350014014") + .withAreaId("1234") + .build(); + + // When + final RestApiException exception = assertThrows(RestApiException.class, () -> routingClient.findDestinations(search)); + + // Then + assertThat(exception.getMessage(), containsString("Only one of ars, ags or areaId must be specified")); + } + + @Test + @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*") + void testFindAreaWithMultipleSearchCriteria() { + + // Given + final ApplicationConfig config = getConfigWithCredentialsFromGitlab("TESTING", true); + + final RoutingClient routingClient = ClientFactory.routingClient(config); + + // When + final List<Area> areas = routingClient.findAreas(List.of("Leip*", "04229"), 0, 10); + + // Then + assertThat(areas, is(not(empty()))); + assertThat(areas.size(), is(lessThanOrEqualTo(10))); + assertTrue(areas.stream().anyMatch(area -> area.getName().equals("Leipzig"))); + } + + } + @Nested class AuthenticationTests { @Test @@ -414,12 +539,12 @@ class ClientIntegrationTest { // Given final var tokenUrl = "https://auth-testing.fit-connect.fitko.dev/token"; - final var clientId = "781f6213-0f0f-4a79-9372-e7187ffda98b"; - final var secret = "PnzR8Vbmhpv_VwTkT34wponqXWK8WBm-LADlryYdV4o"; - final var scope1 = "send:region:DE"; - final var scope2 = "send:region:EN"; + final var clientId = System.getenv("SUBSCRIBER_CLIENT_ID"); + final var secret = System.getenv("SUBSCRIBER_CLIENT_SECRET"); + + final RestService restService = new RestService(new BuildInfo()); - final var authService = new DefaultOAuthService(new RestTemplate(), clientId, secret, tokenUrl); + final var authService = new DefaultOAuthService(restService.getRestTemplate(), clientId, secret, tokenUrl); // When final OAuthToken token = authService.getCurrentToken(); diff --git a/client/src/test/java/dev/fitko/fitconnect/client/RoutingClientTest.java b/client/src/test/java/dev/fitko/fitconnect/client/RoutingClientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a0ee2b95c8f06a868455ae56c7707f4917b4697a --- /dev/null +++ b/client/src/test/java/dev/fitko/fitconnect/client/RoutingClientTest.java @@ -0,0 +1,138 @@ +package dev.fitko.fitconnect.client; + +import dev.fitko.fitconnect.api.domain.model.route.Area; +import dev.fitko.fitconnect.api.domain.model.route.AreaResult; +import dev.fitko.fitconnect.api.domain.model.route.Route; +import dev.fitko.fitconnect.api.domain.model.route.RouteResult; +import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.RoutingException; +import dev.fitko.fitconnect.api.services.routing.RoutingService; +import dev.fitko.fitconnect.client.router.DestinationSearch; +import dev.fitko.fitconnect.core.routing.RouteVerifier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RoutingClientTest { + + + RoutingService routingServiceMock; + + RouteVerifier routeVerifierMock; + RoutingClient underTest; + + @BeforeEach + void setup() { + routingServiceMock = mock(RoutingService.class); + routeVerifierMock = mock(RouteVerifier.class); + underTest = new RoutingClient(routingServiceMock, routeVerifierMock); + } + + @Test + void findDestinationsTest() { + + // Given + final var firstExpectedRoute = new Route(); + firstExpectedRoute.setDestinationId(UUID.randomUUID()); + + final var secondExpectedRoute = new Route(); + secondExpectedRoute.setDestinationId(UUID.randomUUID()); + + final var expectedRouteResult = new RouteResult(); + expectedRouteResult.setRoutes(List.of(firstExpectedRoute, secondExpectedRoute)); + + when(routingServiceMock.getRoutes(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(expectedRouteResult); + when(routeVerifierMock.validateRouteDestinations(any(), any(), any())).thenReturn(ValidationResult.ok()); + + final DestinationSearch search = DestinationSearch.Builder() + .withLeikaKey("99400048079000") + .withArs("064350014014") + .withLimit(5) + .build(); + + // When + final List<Route> destinations = underTest.findDestinations(search); + + // Then + assertThat(destinations.size(), is(2)); + assertThat(destinations.get(0).getDestinationId(), is(firstExpectedRoute.getDestinationId())); + assertThat(destinations.get(1).getDestinationId(), is(secondExpectedRoute.getDestinationId())); + } + + @Test + void findDestinationsWithFailedValidationTest() { + + // Given + final var expectedRoute = new Route(); + expectedRoute.setDestinationId(UUID.randomUUID()); + + final var expectedRouteResult = new RouteResult(); + expectedRouteResult.setRoutes(List.of(expectedRoute)); + + when(routingServiceMock.getRoutes(any(), any(), any(), any(), anyInt(), anyInt())).thenReturn(expectedRouteResult); + when(routeVerifierMock.validateRouteDestinations(any(), any(), any())).thenReturn(ValidationResult.error(new RoutingException("Route validation failed"))); + + final DestinationSearch search = DestinationSearch.Builder() + .withLeikaKey("99400048079000") + .withArs("064350014014") + .withLimit(5) + .build(); + + // When + final RoutingException exception = assertThrows(RoutingException.class, () -> underTest.findDestinations(search)); + + // Then + assertThat(exception.getMessage(), containsString("Route validation failed")); + } + + @Test + void testGetAreasWithSingleSearchExpressionTest() { + // Given + final var expectedArea = new Area(); + expectedArea.setName("Leipzig"); + + final var expectedAreaResult = new AreaResult(); + expectedAreaResult.setAreas(List.of(expectedArea)); + + when(routingServiceMock.getAreas(anyList(), anyInt(), anyInt())).thenReturn(expectedAreaResult); + + // When + final List<Area> areas = underTest.findAreas("04229", 0, 5); + + // Then + assertThat(areas.size(), is(1)); + assertThat(areas.get(0).getName(), is("Leipzig")); + } + + @Test + void getAreasWithMultipleFiltersTest() { + + // Given + final var expectedArea = new Area(); + expectedArea.setName("Leipzig"); + + final var expectedAreaResult = new AreaResult(); + expectedAreaResult.setAreas(List.of(expectedArea)); + + when(routingServiceMock.getAreas(anyList(), anyInt(), anyInt())).thenReturn(expectedAreaResult); + + // When + final List<Area> areas = underTest.findAreas(List.of("Leip*", "04229"), 0, 5); + + // Then + assertThat(areas.size(), is(1)); + assertThat(areas.get(0).getName(), is("Leipzig")); + } +} \ No newline at end of file diff --git a/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java b/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java index e6f56c69fe2f19363f563836847ca1950bf5c1ce..d9a9c1c70788fe5c52f2ef292282faea1b68fd55 100644 --- a/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java +++ b/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java @@ -121,7 +121,6 @@ public class SenderClientTest { when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); when(senderMock.validateXmlFormat(any())).thenReturn(ValidationResult.ok()); - // When final var submission = SubmissionBuilder.Builder() .withAttachment(new File("src/test/resources/attachment.txt")) @@ -162,92 +161,19 @@ public class SenderClientTest { logs.assertContains("Detected attachment mime-type application/pdf"); } - - @Test - void testWithMissingData() throws Exception { - - // Given - final UUID destinationId = setupTestMocks(); - - // When - final var submission = SubmissionBuilder.Builder() - .withJsonData(null) - .withDestination(destinationId) - .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000") - .build(); - - // When - final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission)); - - // Then - assertThat(exception.getMessage(), containsString("Data is mandatory, but was null")); - } - - @Test - void testWithMissingDestinationId() throws Exception { - - // Given - setupTestMocks(); - - // When - final var submission = SubmissionBuilder.Builder() - .withJsonData("{}") - .withDestination(null) - .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000") - .build(); - - // When - final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission)); - - // Then - assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null")); - } - - @Test - void testWithInvalidDestinationId() { - - // Given - final var invalidUUID = "1234:132434:5678"; - - // When - final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> - SubmissionBuilder.Builder() - .withJsonData("{}") - .withDestination(UUID.fromString(invalidUUID)) - .withServiceType("name", "test:key") - .build()); - - // Then - assertThat(exception.getMessage(), containsString("Invalid UUID string: " + invalidUUID)); - } - - @Test - void testWithInvalidLeikaKey() throws JOSEException { - - // Given - setupTestMocks(); - - // Given - final var submission = SubmissionBuilder.Builder() - .withJsonData("{\"test\" . \"data\"}") - .withDestination(UUID.randomUUID()) - .withServiceType("name", "illegal:test:identifier") - .build(); - - // When - final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission)); - - // Then - assertThat(exception.getMessage(), containsString("LeikaKey has invalid format")); - } - @Test void testMissingDataMimeTypeSchema() throws Exception { // Given final var destinationId = UUID.randomUUID(); + + final var destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + final var destination = new Destination(); + destination.setServices(Set.of(destinationService)); destination.setDestinationId(destinationId); + final var announcedSubmission = getAnnouncedSubmission(destinationId); final RSAKey publicKey = generateRsaKey(4096).toPublicJWK(); @@ -272,68 +198,6 @@ public class SenderClientTest { logs.assertContains("Required schema to send valid submission not found"); } - @Test - void testWithMissingServiceTypeIdentifier() throws Exception { - - // Given - final var destinationId = setupTestMocks(); - - final var submission = SubmissionBuilder.Builder() - .withJsonData("{}") - .withDestination(destinationId) - .withServiceType("Name", null) - .build(); - - // When - final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission)); - - // Then - assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null")); - } - - @Test - void testMissingEncryptedMetadata() throws Exception { - - // Given - final var destinationId = setupTestMocks(); - - // When - final EncryptedSubmissionPayload submission = EncryptedSubmissionBuilder.Builder() - .withEncryptedData("encr$&ted d/&)ata") - .withEncryptedMetadata(null) - .withDestination(destinationId) - .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000") - .build(); - - // When - final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission)); - - // Then - assertThat(exception.getMessage(), containsString("Encrypted metadata must not be null")); - } - - @Test - void testMissingEncryptedData() throws Exception { - - // Given - final var destinationId = setupTestMocks(); - - // When - final EncryptedSubmissionPayload submission = EncryptedSubmissionBuilder.Builder() - .withEncryptedData(null) - .withEncryptedMetadata("encr$&ted met@d/&)ata") - .withDestination(destinationId) - .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000") - .build(); - - // When - final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> senderClient.submit(submission)); - - // Then - assertThat(exception.getMessage(), containsString("Data is mandatory, but was null")); - } - - @Test void testWithEmptyAttachments() throws Exception { @@ -445,7 +309,7 @@ public class SenderClientTest { // Given final var destinationId = setupTestMocks(); - when(senderMock.getDestination(any())).thenThrow(new RestApiException("Loading destination failed")); + when(senderMock.createSubmission(any())).thenThrow(new RestApiException("Announcing submission failed")); // When final SubmissionPayload submission = SubmissionBuilder.Builder() @@ -635,6 +499,7 @@ public class SenderClientTest { } private UUID setupTestMocks() throws JOSEException { + final var destinationId = UUID.randomUUID(); final var destination = getDestination(destinationId); final var announcedSubmission = getAnnouncedSubmission(destinationId); @@ -674,6 +539,7 @@ public class SenderClientTest { schema.setMimeType(MimeType.APPLICATION_JSON); final var destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); destinationService.setSubmissionSchemas(Set.of(schema)); final var destination = new Destination(); @@ -684,6 +550,7 @@ public class SenderClientTest { private Destination getDestination(final UUID destinationId, final SubmissionSchema schema) { final var destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); destinationService.setSubmissionSchemas(Set.of(schema)); final var destination = new Destination(); diff --git a/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java b/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java index 82e9749dbe451b93647e851cd35de0ba60d9ce40..9c1c539a6704aee684d822e66952c3d384d3f36c 100644 --- a/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java +++ b/client/src/test/java/dev/fitko/fitconnect/client/factory/ClientFactoryTest.java @@ -16,6 +16,7 @@ class ClientFactoryTest { void testMissingConfiguration() { assertThrows(InitializationException.class, ClientFactory::senderClient); assertThrows(InitializationException.class, ClientFactory::subscriberClient); + assertThrows(InitializationException.class, ClientFactory::routingClient); } @Test @@ -61,6 +62,23 @@ class ClientFactoryTest { assertNotNull(ClientFactory.subscriberClient(subscriberConfig)); } + @Test + void testRoutingClientConstruction() { + + final var envName = new EnvironmentName("DEV"); + final var environment = new Environment("", "https://routing.fitko.fitconnect.de", "", "", true); + + final var senderConfig = new SenderConfig("1234", "abcd"); + + final var routingConfig = ApplicationConfig.builder() + .environments(Map.of(envName, environment)) + .activeEnvironment(envName) + .senderConfig(senderConfig) + .build(); + + assertNotNull(ClientFactory.routingClient(routingConfig)); + } + @Test void testSigningKeyCannotBeParsed() { diff --git a/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java b/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java index 2c2428fc46829b6f1fb301cee976f1b09ab9ae18..200b2e2e9d3d519880a83d80658f218835da2ccf 100644 --- a/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java +++ b/client/src/test/java/dev/fitko/fitconnect/client/testutil/LogCaptor.java @@ -6,7 +6,6 @@ import ch.qos.logback.core.AppenderBase; import java.util.ArrayList; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public final class LogCaptor extends AppenderBase<LoggingEvent> { diff --git a/client/src/test/java/dev/fitko/fitconnect/client/util/SubmissionUtilTest.java b/client/src/test/java/dev/fitko/fitconnect/client/util/SubmissionUtilTest.java deleted file mode 100644 index b9eda5af746b0021a00985a99bdbb277ccba6c04..0000000000000000000000000000000000000000 --- a/client/src/test/java/dev/fitko/fitconnect/client/util/SubmissionUtilTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.fitko.fitconnect.client.util; - -import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; -import dev.fitko.fitconnect.api.domain.model.metadata.PublicServiceType; -import dev.fitko.fitconnect.api.domain.model.metadata.Signature; -import dev.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; -import dev.fitko.fitconnect.api.domain.model.metadata.data.Data; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static dev.fitko.fitconnect.api.config.SchemaConfig.METADATA_V_1_0_0; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -public class SubmissionUtilTest { - - @Test - public void buildMetadata() { - - Signature signature = new Signature(); - signature.setContent("content"); - Data data = new Data(); - data.setSignature(signature); - - Attachment attachment1 = new Attachment(); - attachment1.setFilename("fileName1"); - Attachment attachment2 = new Attachment(); - attachment2.setFilename("fileName2"); - List<Attachment> attachments = List.of(attachment1, attachment2); - - PublicServiceType publicServiceType = new PublicServiceType(); - publicServiceType.setName("publicService"); - - Metadata metadata = SubmissionUtil.buildMetadata(attachments, data, publicServiceType); - - assertThat(metadata.getSchema(), is(METADATA_V_1_0_0.toString())); - assertThat(metadata.getContentStructure().getAttachments().size(), is(2)); - assertThat(metadata.getContentStructure().getAttachments().get(0).getFilename(), is("fileName1")); - assertThat(metadata.getContentStructure().getAttachments().get(1).getFilename(), is("fileName2")); - assertThat(metadata.getContentStructure().getData().getSignature().getContent(), is("content")); - assertThat(metadata.getPublicServiceType().getName(), is("publicService")); - } -} diff --git a/client/src/test/java/dev/fitko/fitconnect/client/util/ValidDataGuardTest.java b/client/src/test/java/dev/fitko/fitconnect/client/util/ValidDataGuardTest.java new file mode 100644 index 0000000000000000000000000000000000000000..86b0e2777d61f8d528da4e38f0d3369c0b26b36c --- /dev/null +++ b/client/src/test/java/dev/fitko/fitconnect/client/util/ValidDataGuardTest.java @@ -0,0 +1,353 @@ +package dev.fitko.fitconnect.client.util; + +import dev.fitko.fitconnect.api.domain.model.destination.Destination; +import dev.fitko.fitconnect.api.domain.model.destination.DestinationService; +import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.services.Sender; +import dev.fitko.fitconnect.client.sender.EncryptedSubmissionBuilder; +import dev.fitko.fitconnect.client.sender.SubmissionBuilder; +import dev.fitko.fitconnect.client.sender.model.EncryptedSubmissionPayload; +import dev.fitko.fitconnect.client.sender.model.SubmissionPayload; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class ValidDataGuardTest { + + ValidDataGuard underTest; + Sender senderMock; + + @BeforeEach + void setUp() { + senderMock = mock(Sender.class); + underTest = new ValidDataGuard(senderMock); + } + + @Nested + class SubmissionPayloadDataTests { + + @Test + void testValidSubmissionPayload() { + + // Given + final DestinationService service = new DestinationService(); + service.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final Destination destination = new Destination(); + destination.setDestinationId(UUID.randomUUID()); + destination.setServices(Set.of(service)); + + when(senderMock.getDestination(any())).thenReturn(destination); + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final SubmissionPayload submissionPayload = SubmissionBuilder.Builder() + .withJsonData("\"test\": \"json\"") + .withDestination(UUID.randomUUID()) + .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // Then valid if no exception is thrown + underTest.ensureValidDataPayload(submissionPayload); + } + + @Test + void testMissingJsonData() { + + // Given + final SubmissionPayload submissionPayload = SubmissionBuilder.Builder() + .withJsonData(null) + .withDestination(UUID.randomUUID()) + .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Data is mandatory, but was null")); + } + + @Test + void testMissingXmlData() { + + // Given + final SubmissionPayload submissionPayload = SubmissionBuilder.Builder() + .withXmlData(null) + .withDestination(UUID.randomUUID()) + .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Data is mandatory, but was null")); + } + + @Test + void testMissingDestinationId() { + + // Given + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final SubmissionPayload submissionPayload = SubmissionBuilder.Builder() + .withJsonData("{}") + .withDestination(null) + .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null")); + } + + @Test + void testInvalidServiceIdentifier() { + + // Given + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final SubmissionPayload submissionPayload = SubmissionBuilder.Builder() + .withJsonData("{\"test\" . \"data\"}") + .withDestination(UUID.randomUUID()) + .withServiceType("name", "illegal:test:identifier") + .build(); + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("LeikaKey has invalid format")); + } + + @Test + void testMissingServiceIdentifier() { + + // Given + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final SubmissionPayload submissionPayload = SubmissionBuilder.Builder() + .withJsonData("{\"test\" . \"data\"}") + .withDestination(UUID.randomUUID()) + .withServiceType("name", null) + .build(); + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null")); + } + + @Test + void testServiceIdentifierNotMatchingDestination() { + + // Given + final DestinationService service = new DestinationService(); + service.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final Destination destination = new Destination(); + destination.setServices(Set.of(service)); + + when(senderMock.getDestination(any())).thenReturn(destination); + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final SubmissionPayload submissionPayload = SubmissionBuilder.Builder() + .withJsonData("\"test\": \"json\"") + .withDestination(UUID.randomUUID()) + .withServiceType("Test", "urn:de:fim:leika:leistung:123456789101114") + .build(); + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(submissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Provided service type 'urn:de:fim:leika:leistung:123456789101114' is not allowed by the destination")); + } + + } + + + @Nested + class EncryptedSubmissionPayloadDataTests { + + + @Test + void testValidEncryptedSubmissionPayload() { + + // Given + final DestinationService service = new DestinationService(); + service.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final Destination destination = new Destination(); + destination.setDestinationId(UUID.randomUUID()); + destination.setServices(Set.of(service)); + + when(senderMock.getDestination(any())).thenReturn(destination); + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder() + .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withDestination(UUID.randomUUID()) + .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // Then valid if no exception is thrown + underTest.ensureValidDataPayload(encryptedSubmissionPayload); + } + + @Test + void testMissingEncryptedData() { + + // Given + final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder() + .withEncryptedData(null) + .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withDestination(UUID.randomUUID()) + .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Encrypted data is mandatory, but was null")); + } + + @Test + void testMissingEncryptedMetadata() { + + // Given + final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder() + .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withEncryptedMetadata(null) + .withDestination(UUID.randomUUID()) + .withServiceType("Test", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Encrypted metadata must not be null")); + } + + @Test + void testMissingDestinationId() { + + // Given + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder() + .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withDestination(null) + .withServiceType("name", "urn:de:fim:leika:leistung:99400048079000") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("DestinationId is mandatory, but was null")); + } + + @Test + void testInvalidServiceIdentifier() { + + // Given + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder() + .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withDestination(UUID.randomUUID()) + .withServiceType("name", "illegal:test:identifier") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("LeikaKey has invalid format")); + } + + @Test + void testMissingServiceIdentifier() { + + // Given + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder() + .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withDestination(UUID.randomUUID()) + .withServiceType("name", null) + .build(); + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Leika key is mandatory, but was null")); + } + + @Test + void testServiceIdentifierNotMatchingDestination() { + + // Given + final DestinationService service = new DestinationService(); + service.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final Destination destination = new Destination(); + destination.setServices(Set.of(service)); + + when(senderMock.getDestination(any())).thenReturn(destination); + when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); + + final EncryptedSubmissionPayload encryptedSubmissionPayload = EncryptedSubmissionBuilder.Builder() + .withEncryptedData("4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withEncryptedMetadata("4Y0sJhadfrQnNZXeS7Pqh73FvtF4Y0sJhadfrQnNZXeS7Pqh73FvtF") + .withDestination(UUID.randomUUID()) + .withServiceType("name", "urn:de:fim:leika:leistung:11111111111111") + .build(); + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> underTest.ensureValidDataPayload(encryptedSubmissionPayload)); + + // Then + assertThat(exception.getMessage(), containsString("Provided service type 'urn:de:fim:leika:leistung:11111111111111' is not allowed by the destination")); + } + } + + @Nested + class MiscTests { + + @Test + void testInvalidDestinationId() { + + // Given + final var invalidUUID = "1234:132434:5678"; + + // When + final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + SubmissionBuilder.Builder() + .withJsonData("{}") + .withDestination(UUID.fromString(invalidUUID)) + .withServiceType("name", "test:key") + .build()); + + // Then + assertThat(exception.getMessage(), containsString("Invalid UUID string: " + invalidUUID)); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java index 5b4d94837ece7ef8773daf624ac8b5113221eab8..f66087eabaf1bc8d1f01c07c79039eefdd130b96 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java @@ -60,8 +60,9 @@ public class SubmissionSender implements Sender { } @Override - public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) { - return this.validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret); + public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) { + LOGGER.info("Validating callback integrity"); + return validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret); } @Override diff --git a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java index d1f74480454709c086ca92589e4c951a20acf64b..c47927ed62883d7403df37fa50ea5429a9d26554 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java @@ -86,8 +86,9 @@ public class SubmissionSubscriber implements Subscriber { } @Override - public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) { - return this.validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret); + public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) { + LOGGER.info("Validating callback integrity"); + return validationService.validateCallback(hmac, timestamp, httpBody, callbackSecret); } @Override diff --git a/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java b/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java index f59a1b858894bd744c757ad0978b9a99b17686bb..908d0b2ce48e155d05f0bac9e9688e082f8e3101 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/auth/DefaultOAuthService.java @@ -38,7 +38,7 @@ public class DefaultOAuthService implements OAuthService { this.restTemplate = restTemplate; this.clientId = clientId; this.clientSecret = clientSecret; - this.tokenUrl = authUrl; + tokenUrl = authUrl; resetExistingToken(); } @@ -48,26 +48,26 @@ public class DefaultOAuthService implements OAuthService { LOGGER.info("Current token is expired, authenticating ..."); authenticate(); } - return this.currentToken; + return currentToken; } private boolean tokenExpired() { - if (this.currentToken == null || this.tokenExpirationTime == null) { + if (currentToken == null || tokenExpirationTime == null) { return true; } final var now = LocalDateTime.now(); - return this.tokenExpirationTime.isBefore(now) || this.tokenExpirationTime.isEqual(now); + return tokenExpirationTime.isBefore(now) || tokenExpirationTime.isEqual(now); } private void resetExistingToken() { - this.currentToken = null; - this.tokenExpirationTime = null; + currentToken = null; + tokenExpirationTime = null; } private void authenticate() throws AuthenticationException { - final String requestBody = buildRequestBody(this.clientId, this.clientSecret); - this.currentToken = performTokenRequest(requestBody); - this.tokenExpirationTime = LocalDateTime.now().plusSeconds(this.currentToken.getExpiresIn()); + final String requestBody = buildRequestBody(clientId, clientSecret); + currentToken = performTokenRequest(requestBody); + tokenExpirationTime = LocalDateTime.now().plusSeconds(currentToken.getExpiresIn()); } private String buildRequestBody(final String clientId, final String clientSecret, final String... scope) { @@ -95,9 +95,9 @@ public class DefaultOAuthService implements OAuthService { final HttpEntity<String> entity = new HttpEntity<>(requestBody, headers); try { LOGGER.info("Sending authentication request"); - return this.restTemplate.exchange(this.tokenUrl, HttpMethod.POST, entity, OAuthToken.class).getBody(); + return restTemplate.exchange(tokenUrl, HttpMethod.POST, entity, OAuthToken.class).getBody(); } catch (final RestClientException e) { - LOGGER.error(e.getMessage(),e); + LOGGER.error(e.getMessage(), e); throw new RestApiException("Could not retrieve OAuth token", e); } } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java index 5cb1aef2bffa6bcc504df1bf5667d74c7634ecef..905a5d52a3cb65adaa5753c0816da5b614863c53 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogVerifier.java @@ -20,11 +20,24 @@ import dev.fitko.fitconnect.api.services.events.EventLogVerificationService; import dev.fitko.fitconnect.api.services.validation.ValidationService; import java.text.ParseException; -import java.util.*; - -import static com.nimbusds.jwt.JWTClaimNames.*; -import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.*; -import static dev.fitko.fitconnect.core.util.EventLogUtil.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static com.nimbusds.jwt.JWTClaimNames.ISSUED_AT; +import static com.nimbusds.jwt.JWTClaimNames.ISSUER; +import static com.nimbusds.jwt.JWTClaimNames.JWT_ID; +import static com.nimbusds.jwt.JWTClaimNames.SUBJECT; +import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_EVENTS; +import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_SCHEMA; +import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.CLAIM_TXN; +import static dev.fitko.fitconnect.api.domain.model.event.EventClaimFields.HEADER_TYPE; +import static dev.fitko.fitconnect.core.util.EventLogUtil.getAuthenticationTags; +import static dev.fitko.fitconnect.core.util.EventLogUtil.getDestinationId; +import static dev.fitko.fitconnect.core.util.EventLogUtil.getEventFromClaims; +import static dev.fitko.fitconnect.core.util.EventLogUtil.resolveIssuerType; public class EventLogVerifier implements EventLogVerificationService { @@ -150,7 +163,7 @@ public class EventLogVerifier implements EventLogVerificationService { private RSAKey getSignatureKey(final String issuer, final String keyId) throws ParseException, EventLogException { final EventIssuer issuerType = resolveIssuerType(issuer); if (issuerType == EventIssuer.SUBMISSION_SERVICE) { - return keyService.getSubmissionServiceSignatureKey(keyId); + return keyService.getSubmissionServicePublicKey(keyId); } else { return keyService.getPublicSignatureKey(getDestinationId(issuer), keyId); } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyRestTemplate.java b/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyRestTemplate.java deleted file mode 100644 index 3c3c0c18ee7e7ae7c9a3c310e6130c29b82eb906..0000000000000000000000000000000000000000 --- a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyRestTemplate.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.fitko.fitconnect.core.http; - -import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -public class ProxyRestTemplate extends RestTemplate { - - public ProxyRestTemplate(final ClientHttpRequestFactory requestFactory) { - super(requestFactory); - this.getMessageConverters().add(new X509CRLHttpMessageConverter()); - } -} diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyConfig.java b/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java similarity index 68% rename from core/src/main/java/dev/fitko/fitconnect/core/http/ProxyConfig.java rename to core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java index a65aa05ebd2510f5f31a5493bee0c933e13a0d56..7711d95adc82632c6b1960827535861278554093 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/http/ProxyConfig.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/http/RestService.java @@ -16,25 +16,25 @@ import java.util.List; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; -public class ProxyConfig { +public class RestService { - private static final Logger LOGGER = LoggerFactory.getLogger(ProxyConfig.class); + private static final Logger LOGGER = LoggerFactory.getLogger(RestService.class); - private BuildInfo buildInfo; - private String host; - private int port; + private final BuildInfo buildInfo; + private final String proxyHost; + private final int proxyPort; - public ProxyConfig(String host, int port, BuildInfo buildInfo) { - this.host = host; - this.port = port; + public RestService(final String proxyHost, final int proxyPort, final BuildInfo buildInfo) { + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; this.buildInfo = buildInfo; } - ProxyConfig() { - this(null, 0, new BuildInfo()); + public RestService(final BuildInfo buildInfo) { + this(null, 0, buildInfo); } - public RestTemplate proxyRestTemplate() { + public RestTemplate getRestTemplate() { return hasProxySet() ? getProxyRestTemplate() : getDefaultRestTemplate(); } @@ -45,41 +45,41 @@ public class ProxyConfig { return restTemplate; } - private ProxyRestTemplate getProxyRestTemplate() { + private RestTemplate getProxyRestTemplate() { LOGGER.info("Using proxy {}", this); final var requestFactory = new SimpleClientHttpRequestFactory(); - final var proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(this.host, this.port)); + final var proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); requestFactory.setProxy(proxy); - final ProxyRestTemplate proxyRestTemplate = new ProxyRestTemplate(requestFactory); + final RestTemplate proxyRestTemplate = new RestTemplate(requestFactory); setupTemplate(proxyRestTemplate); return proxyRestTemplate; } - private void setMappingConverter(final RestTemplate restTemplate) { + private void setMappingConverters(final RestTemplate restTemplate) { final MappingJackson2HttpMessageConverter jacksonMessageConverter = new MappingJackson2HttpMessageConverter(); jacksonMessageConverter.setObjectMapper(new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false)); restTemplate.getMessageConverters().add(jacksonMessageConverter); } - private void setLoggingInterceptor(final RestTemplate restTemplate) { + private void setLoggingInterceptors(final RestTemplate restTemplate) { final List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors(); interceptors.add(new ApiRequestInterceptor()); - interceptors.add(new UserAgentInterceptor(this.buildInfo)); + interceptors.add(new UserAgentInterceptor(buildInfo)); restTemplate.setInterceptors(interceptors); } private void setupTemplate(final RestTemplate restTemplate) { - setLoggingInterceptor(restTemplate); - setMappingConverter(restTemplate); + setLoggingInterceptors(restTemplate); + setMappingConverters(restTemplate); } boolean hasProxySet() { - return !Strings.isNullOrEmpty(this.host) && this.port > 0; + return !Strings.isNullOrEmpty(proxyHost) && proxyPort > 0; } @Override public String toString() { - return String.format("ProxyConfig {host='%s', port=%d}", this.host, this.port); + return String.format("ProxyConfig {host='%s', port=%d}", proxyHost, proxyPort); } } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/http/X509CRLHttpMessageConverter.java b/core/src/main/java/dev/fitko/fitconnect/core/http/X509CRLHttpMessageConverter.java deleted file mode 100644 index bc41d6f3650053304b8306a09aaccb4bb5300503..0000000000000000000000000000000000000000 --- a/core/src/main/java/dev/fitko/fitconnect/core/http/X509CRLHttpMessageConverter.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.fitko.fitconnect.core.http; - -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.MediaType; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.lang.NonNull; - -import java.security.cert.CertificateFactory; -import java.security.cert.X509CRL; - -public class X509CRLHttpMessageConverter extends AbstractHttpMessageConverter<X509CRL> { - - public X509CRLHttpMessageConverter() { - super(new MediaType("application", "pkix-crl")); - } - - @Override - protected boolean supports(@NonNull final Class<?> clazz) { - return X509CRL.class == clazz; - } - - @Override - protected X509CRL readInternal(@NonNull final Class<? extends X509CRL> clazz, @NonNull final HttpInputMessage inputMessage) - throws HttpMessageNotReadableException { - try { - final var cf = CertificateFactory.getInstance("X.509"); - return (X509CRL) cf.generateCRL(inputMessage.getBody()); - } catch (final Exception e) { - throw new HttpMessageNotReadableException("CertificateFactory of type X.509 could not be created", inputMessage); - } - } - - @Override - protected void writeInternal(@NonNull final X509CRL x509CRL, @NonNull final HttpOutputMessage outputMessage) - throws HttpMessageNotWritableException { - throw new HttpMessageNotWritableException("Writing X509CRL is not supported in ProxyRestTemplate"); - } - -} diff --git a/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java b/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java index 40c773fd25eed619654524a758958179d6856829..2ab47f490335b6d9aee31e5e76e7274b0867de14 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/keys/PublicKeyService.java @@ -57,7 +57,7 @@ public class PublicKeyService implements KeyService { public RSAKey getPublicEncryptionKey(final UUID destinationId) { final Destination destination = submissionService.getDestination(destinationId); final String destinationUrl = config.getDestinationsKeyEndpoint(); - final ApiJwk publicKey = performRequest(destinationUrl, ApiJwk.class, destinationId, destination.getEncryptionKid()); + final ApiJwk publicKey = performRequest(destinationUrl, ApiJwk.class, getHeaders(), destinationId, destination.getEncryptionKid()); final RSAKey rsaKey = toRSAKey(publicKey); validateEncryptionKey(rsaKey); return rsaKey; @@ -66,25 +66,33 @@ public class PublicKeyService implements KeyService { @Override public RSAKey getPublicSignatureKey(final UUID destinationId, final String keyId) { final String destinationUrl = config.getDestinationsKeyEndpoint(); - final ApiJwk signatureKey = performRequest(destinationUrl, ApiJwk.class, destinationId, keyId); + final ApiJwk signatureKey = performRequest(destinationUrl, ApiJwk.class, getHeaders(), destinationId, keyId); final RSAKey rsaKey = toRSAKey(signatureKey); validateSignatureKey(rsaKey); return rsaKey; } @Override - public RSAKey getSubmissionServiceSignatureKey(final String keyId) { + public RSAKey getSubmissionServicePublicKey(final String keyId) { final String submissionServiceUrl = config.getSubmissionServiceWellKnownKeysEndpoint(); - final ApiJwkSet wellKnownKeys = performRequest(submissionServiceUrl, ApiJwkSet.class); + final ApiJwkSet wellKnownKeys = performRequest(submissionServiceUrl, ApiJwkSet.class, getHeaders()); final RSAKey signatureKey = filterKeysById(keyId, wellKnownKeys.getKeys()); validateSignatureKey(signatureKey); return signatureKey; } @Override - public RSAKey getPortalSignatureKey(final String keyId) { + public RSAKey getPortalPublicKey(final String keyId) { final String portalUrl = config.getSelfServicePortalWellKnownKeysEndpoint(); - final ApiJwkSet wellKnownKeys = performRequest(portalUrl, ApiJwkSet.class); + final ApiJwkSet wellKnownKeys = performRequest(portalUrl, ApiJwkSet.class, getHeaders()); + final RSAKey signatureKey = filterKeysById(keyId, wellKnownKeys.getKeys()); + validateSignatureKey(signatureKey); + return signatureKey; + } + @Override + public RSAKey getWellKnownKeysForSubmissionUrl(final String url, final String keyId) { + final var requestUrl = !url.endsWith("/") ? url + config.getWellKnownKeysPath() : url; + final ApiJwkSet wellKnownKeys = performRequest(requestUrl, ApiJwkSet.class, getHeadersWithoutAuth(), keyId); final RSAKey signatureKey = filterKeysById(keyId, wellKnownKeys.getKeys()); validateSignatureKey(signatureKey); return signatureKey; @@ -126,8 +134,7 @@ public class PublicKeyService implements KeyService { } } - private <T> T performRequest(final String url, final Class<T> responseType, final Object... params) { - final HttpHeaders headers = getHeaders(); + private <T> T performRequest(final String url, final Class<T> responseType, final HttpHeaders headers, final Object... params) { final HttpEntity<String> entity = new HttpEntity<>(headers); try { return restTemplate.exchange(url, HttpMethod.GET, entity, responseType, params).getBody(); @@ -136,11 +143,15 @@ public class PublicKeyService implements KeyService { } } - private HttpHeaders getHeaders() { + private HttpHeaders getHeadersWithoutAuth() { final var headers = new HttpHeaders(); - headers.setBearerAuth(authService.getCurrentToken().getAccessToken()); headers.setContentType(MediaType.APPLICATION_JSON); headers.setAcceptCharset(List.of(StandardCharsets.UTF_8)); return headers; } + private HttpHeaders getHeaders() { + final var headers = getHeadersWithoutAuth(); + headers.setBearerAuth(authService.getCurrentToken().getAccessToken()); + return headers; + } } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/routing/RouteVerifier.java b/core/src/main/java/dev/fitko/fitconnect/core/routing/RouteVerifier.java new file mode 100644 index 0000000000000000000000000000000000000000..b1920628b492018dd022329d4126b9edc7c6f94c --- /dev/null +++ b/core/src/main/java/dev/fitko/fitconnect/core/routing/RouteVerifier.java @@ -0,0 +1,195 @@ +package dev.fitko.fitconnect.core.routing; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import dev.fitko.fitconnect.api.domain.model.route.Route; +import dev.fitko.fitconnect.api.domain.model.route.RouteDestination; +import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.InvalidKeyException; +import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.exceptions.ValidationException; +import dev.fitko.fitconnect.api.services.keys.KeyService; +import dev.fitko.fitconnect.api.services.routing.RoutingVerificationService; +import dev.fitko.fitconnect.api.services.validation.ValidationService; +import dev.fitko.fitconnect.core.util.Strings; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static dev.fitko.fitconnect.api.domain.validation.ValidationResult.error; +import static dev.fitko.fitconnect.api.domain.validation.ValidationResult.ok; + +public class RouteVerifier implements RoutingVerificationService { + + private static final JsonMapper MAPPER = getConfiguredJsonMapper(); + private final KeyService keyService; + private final ValidationService validationService; + + public RouteVerifier(final KeyService keyService, final ValidationService validationService) { + this.keyService = keyService; + this.validationService = validationService; + } + + @Override + public ValidationResult validateRouteDestinations(final List<Route> routes, final String requestedServiceIdentifier, final String requestedRegion) { + return routes.stream() + .map(route -> validateRoute(route, requestedServiceIdentifier, requestedRegion)) + .filter(ValidationResult::hasError) + .findFirst().orElse(ValidationResult.ok()); + } + + private ValidationResult validateRoute(final Route route, final String requestedServiceIdentifier, final String requestedRegion) { + try { + validateDestinationSignature(route, requestedServiceIdentifier, requestedRegion); + validateDestinationParameterSignature(route); + return ok(); + } catch (final ValidationException e) { + return error(e); + } catch (final InvalidKeyException e) { + return error(new ValidationException("Public signature key is invalid: " + e.getMessage())); + } catch (final RestApiException e) { + return error(new ValidationException("Could not retrieve public signature key: " + e.getMessage())); + } catch (final ParseException | JOSEException | JsonProcessingException e) { + return error(new ValidationException("Signature processing failed: " + e.getMessage())); + } + } + + private void validateDestinationParameterSignature(final Route route) throws JOSEException, ParseException, JsonProcessingException { + final SignedJWT completedSignature = combineDetachedSignatureWithPayload(route); + final RSAKey publicSignatureKey = loadPublicKey(route, completedSignature); + if (!completedSignature.verify(new RSASSAVerifier(publicSignatureKey))) { + throw new ValidationException("Invalid destination parameter signature for route " + route.getDestinationId()); + } + checkHeaderAlgorithm(completedSignature.getHeader()); + } + + private RSAKey loadPublicKey(final Route route, final SignedJWT completedSignature) { + final String keyId = completedSignature.getHeader().getKeyID(); + final String submissionUrl = route.getDestinationParameters().getSubmissionUrl(); + return keyService.getWellKnownKeysForSubmissionUrl(submissionUrl, keyId); + } + + private SignedJWT combineDetachedSignatureWithPayload(final Route route) throws ParseException, JsonProcessingException { + final SignedJWT detachedSignature = SignedJWT.parse(route.getDestinationParametersSignature()); + final Base64URL encodedDetachedPayloadPart = getBase64EncodedDetachedPayload(route); + final Base64URL headerPart = detachedSignature.getHeader().getParsedBase64URL(); + final Base64URL signaturePart = detachedSignature.getSignature(); + return new SignedJWT(headerPart, encodedDetachedPayloadPart, signaturePart); + + } + + private Base64URL getBase64EncodedDetachedPayload(final Route route) throws JsonProcessingException { + final RouteDestination detachedPayload = route.getDestinationParameters(); + final String cleanedDetachedPayload = Strings.cleanNonPrintableChars(MAPPER.writeValueAsString(detachedPayload)); + // FIXME email vs. eMail difference between DVDV and SubmissionAPI -> https://git.fitko.de/fit-connect/planning/-/issues/601 + return Base64URL.encode(cleanedDetachedPayload.replace("eMail", "email").getBytes(StandardCharsets.UTF_8)); + } + + private void validateDestinationSignature(final Route route, final String requestedServiceIdentifier, final String requestedRegion) throws ParseException, JOSEException, JsonProcessingException { + final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature()); + + final JWSHeader header = signature.getHeader(); + final JWTClaimsSet claims = signature.getJWTClaimsSet(); + final String submissionUrl = route.getDestinationParameters().getSubmissionUrl(); + + checkHeaderAlgorithm(header); + validatePayloadSchema(claims); + checkMatchingSubmissionHost(claims, submissionUrl); + checkExpectedServices(claims, requestedServiceIdentifier, requestedRegion); + validateAgainstPublicKey(signature, header.getKeyID()); + } + + private void validatePayloadSchema(final JWTClaimsSet claims) { + final ValidationResult validationResult = validationService.validateDestinationSchema(claims.toJSONObject()); + if (validationResult.hasError()) { + throw new ValidationException(validationResult.getError().getMessage(), validationResult.getError()); + } + } + + private void validateAgainstPublicKey(final SignedJWT signature, final String keyId) throws JOSEException { + final RSAKey portalPublicKey = keyService.getPortalPublicKey(keyId); + if (!signature.verify(new RSASSAVerifier(portalPublicKey))) { + throw new ValidationException("Invalid destination signature for public key id " + keyId); + } + } + + private void checkExpectedServices(final JWTClaimsSet claims, final String requestedServiceIdentifier, final String requestedRegion) { + final Map services = (Map) ((ArrayList<?>) (claims.getClaim("services"))).get(0); + final List<String> areaIds = mapIdentifiersToNumericIds(services, "gebietIDs"); + final List<String> serviceIds = mapIdentifiersToNumericIds(services, "leistungIDs"); + if (requestedRegion != null && !areaIds.contains(getIdFromIdentifier(requestedRegion))) { + throw new ValidationException("Requested region '" + requestedRegion + "' is not supported by the destinations services"); + } + if (!serviceIds.contains(getIdFromIdentifier(requestedServiceIdentifier))) { + throw new ValidationException("Requested service identifier '" + requestedServiceIdentifier + "' is not supported by the destinations services"); + } + } + + private static List<String> mapIdentifiersToNumericIds(final Map services, final String claim) { + return ((List<String>) services.get(claim)).stream().map(RouteVerifier::getIdFromIdentifier).collect(Collectors.toList()); + } + + private static String getIdFromIdentifier(final String identifier) { + if (isNumericId(identifier)) { + return identifier; + } + return Arrays.stream(identifier.split(":")) + .reduce((first, second) -> second) + .orElse(null); + } + + private static boolean isNumericId(final String identifier) { + return Pattern.compile("\\d+").matcher(identifier).matches(); + } + + private void checkHeaderAlgorithm(final JWSHeader header) { + if (!header.getAlgorithm().equals(JWSAlgorithm.PS512)) { + throw new ValidationException("Algorithm in signature header is not " + JWSAlgorithm.PS512); + } + } + + private void checkMatchingSubmissionHost(final JWTClaimsSet claims, final String submissionUrl) throws ParseException { + final String submissionHostClaim = claims.getStringClaim("submissionHost"); + final String submissionUrlHost = getHostFromSubmissionUrl(submissionUrl); + if (!submissionUrlHost.equals(submissionHostClaim)) { + throw new ValidationException("Submission host does not match destinationParameters submission url " + submissionHostClaim); + } + } + + private static String getHostFromSubmissionUrl(final String submissionUrl) { + if (submissionUrl == null) { + throw new ValidationException("SubmissionUrl must not be null"); + } + try { + return URI.create(submissionUrl).getHost(); + } catch (final IllegalArgumentException e) { + throw new ValidationException("SubmissionUrl could not be parsed", e); + } + } + + private static JsonMapper getConfiguredJsonMapper() { + return JsonMapper.builder() + .configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true) + .configure(SerializationFeature.INDENT_OUTPUT, false) + .serializationInclusion(JsonInclude.Include.NON_NULL) + .build(); + } +} diff --git a/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java new file mode 100644 index 0000000000000000000000000000000000000000..5639d27979c32fbed0d376736b8bbf82d4f1688c --- /dev/null +++ b/core/src/main/java/dev/fitko/fitconnect/core/routing/RoutingApiService.java @@ -0,0 +1,107 @@ +package dev.fitko.fitconnect.core.routing; + +import dev.fitko.fitconnect.api.config.ApplicationConfig; +import dev.fitko.fitconnect.api.domain.model.route.AreaResult; +import dev.fitko.fitconnect.api.domain.model.route.RouteResult; +import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.services.routing.RoutingService; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static java.util.function.Predicate.not; + +public class RoutingApiService implements RoutingService { + + private final RestTemplate restTemplate; + private final ApplicationConfig config; + + public RoutingApiService(final ApplicationConfig config, final RestTemplate restTemplate) { + this.config = config; + this.restTemplate = restTemplate; + } + + @Override + public AreaResult getAreas(final List<String> searchExpressions, final int offset, final int limit) { + + final String url = config.getAreaEndpoint(); + final HttpHeaders headers = getHttpHeaders(MediaType.parseMediaType("application/problem+json")); + final HttpEntity<String> entity = new HttpEntity<>(headers); + + final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(url); + searchExpressions.forEach(search -> uriBuilder.queryParam("areaSearchexpression", search)); + uriBuilder.queryParam("offset", offset); + uriBuilder.queryParam("limit", limit); + + try { + return restTemplate.exchange(uriBuilder.toUriString(), HttpMethod.GET, entity, AreaResult.class).getBody(); + } catch (final RestClientException e) { + throw new RestApiException("Area query failed", e); + } + } + + @Override + public RouteResult getRoutes(final String leikaKey, final String ars, final String ags, final String areaId, final int offset, final int limit) { + + testIfSearchCriterionIsSet(ars, ags, areaId); + + final String url = config.getRoutesEndpoint(); + final HttpHeaders headers = getHttpHeaders(MediaType.APPLICATION_JSON); + final HttpEntity<String> entity = new HttpEntity<>(headers); + + final UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(url); + uriBuilder.queryParam("leikaKey", leikaKey); + + if (ars != null) { + uriBuilder.queryParam("ars", ars); + } + if (ags != null) { + uriBuilder.queryParam("ags", ags); + } + if (areaId != null) { + uriBuilder.queryParam("areaId", areaId); + } + + uriBuilder.queryParam("offset", offset); + uriBuilder.queryParam("limit", limit); + + try { + return restTemplate.exchange(uriBuilder.toUriString(), HttpMethod.GET, entity, RouteResult.class).getBody(); + } catch (final RestClientException e) { + throw new RestApiException("Route query failed", e); + } + } + + private static void testIfSearchCriterionIsSet(final String ars, final String ags, final String areaId) { + final List<String> areaSearchCriteria = Arrays.asList(ars, ags, areaId); + + if(areaSearchCriteria.stream().allMatch(CriterionEmpty())){ + throw new RestApiException("At least one search criterion out of ags, ars or areaId must be set"); + } + + if(areaSearchCriteria.stream().filter(not(CriterionEmpty())).collect(Collectors.toSet()).size() > 1){ + throw new RestApiException("Only one of ars, ags or areaId must be specified."); + } + } + + private static Predicate<String> CriterionEmpty() { + return criterion -> criterion == null || criterion.isEmpty(); + } + + private HttpHeaders getHttpHeaders(final MediaType mediaType) { + final var headers = new HttpHeaders(); + headers.setAccept(List.of(mediaType)); + headers.setAcceptCharset(List.of(StandardCharsets.UTF_8)); + return headers; + } +} diff --git a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java index 873f2a82f7c37783798584cbf20224c29e1ba9d8..88433e047792bb8e919a386d33a4ba6b1b53ee3c 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/schema/SchemaResourceProvider.java @@ -3,6 +3,7 @@ package dev.fitko.fitconnect.core.schema; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import dev.fitko.fitconnect.api.config.SchemaConfig; +import dev.fitko.fitconnect.api.domain.schema.SchemaResources; import dev.fitko.fitconnect.api.exceptions.InitializationException; import dev.fitko.fitconnect.api.exceptions.SchemaNotFoundException; import dev.fitko.fitconnect.api.services.schema.SchemaProvider; @@ -16,38 +17,50 @@ import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class SchemaResourceProvider implements SchemaProvider { private static final Logger LOGGER = LoggerFactory.getLogger(SchemaResourceProvider.class); + private static final Pattern ALLOWED_SCHEMA_PATTERN = Pattern.compile("1.\\d+.\\d+"); + private final Map<URI, String> setSchemas; private final Map<URI, String> metadataSchemas; + private final Map<URI, String> destinationSchemas; + private static final ObjectMapper MAPPER = new ObjectMapper(); - public SchemaResourceProvider(final List<String> setSchemaFiles, final List<String> metadataSchemaFiles) { + public SchemaResourceProvider(final SchemaResources schemaResources) { setSchemas = new HashMap<>(); metadataSchemas = new HashMap<>(); - populateSetSchemas(setSchemaFiles); - populateMetadataSchemas(metadataSchemaFiles); + destinationSchemas = new HashMap<>(); + populateSetSchemas(schemaResources.getSetSchemaPaths()); + populateMetadataSchemas(schemaResources.getMetadataSchemaPaths()); + populateDestinationSchemas(schemaResources.getDestinationSchemaPaths()); } - private void populateMetadataSchemas(final List<String> metadataSchemaFiles) { + private void populateMetadataSchemas(final List<String> metadataSchemaPaths) { LOGGER.info("Initializing metadata schemas"); - getResourceFiles(metadataSchemaFiles).forEach(this::addMetadataSchema); + getResourceFiles(metadataSchemaPaths).forEach(this::addMetadataSchema); } - private void populateSetSchemas(final List<String> setSchemaFiles) { + private void populateSetSchemas(final List<String> setSchemaPaths) { LOGGER.info("Initializing set schemas"); - getResourceFiles(setSchemaFiles).forEach(this::addSetSchema); + getResourceFiles(setSchemaPaths).forEach(this::addSetSchema); + } + + private void populateDestinationSchemas(final List<String> destinationSchemaPaths) { + LOGGER.info("Initializing destination schemas"); + getResourceFiles(destinationSchemaPaths).forEach(this::addDestinationSchema); } @Override public boolean isAllowedSetSchema(final URI schemaUri) { - return schemaHasMajorVersion(schemaUri, "1"); + return schemaVersionMatchesPattern(schemaUri, ALLOWED_SCHEMA_PATTERN); } @Override @@ -74,6 +87,15 @@ public class SchemaResourceProvider implements SchemaProvider { return schema; } + @Override + public String loadDestinationSchema(final URI schemaUri) throws SchemaNotFoundException { + final String schema = destinationSchemas.get(schemaUri); + if (schema == null) { + throw new SchemaNotFoundException("Destination schema " + schemaUri.toString() + " is not available."); + } + return schema; + } + private void addSetSchema(final String schema) { setSchemas.put(readIdFromSchema(schema), schema); } @@ -82,6 +104,10 @@ public class SchemaResourceProvider implements SchemaProvider { metadataSchemas.put(readIdFromSchema(schema), schema); } + private void addDestinationSchema(final String schema) { + destinationSchemas.put(readIdFromSchema(schema), schema); + } + private URI readIdFromSchema(final String schema) { try { return URI.create(MAPPER.readTree(schema).get("$id").asText()); @@ -102,8 +128,9 @@ public class SchemaResourceProvider implements SchemaProvider { throw new InitializationException("Could not read schema file " + schemaFile, e); } } - private boolean schemaHasMajorVersion(final URI schemaUri, final String majorVersion) { + + private boolean schemaVersionMatchesPattern(final URI schemaUri, final Pattern pattern) { final String schemaVersion = schemaUri.getPath().split("/")[3]; - return schemaVersion.startsWith(majorVersion + "."); + return pattern.matcher(schemaVersion).matches(); } } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java index 35725f737416b8a3f84564431ab82e6f0e1a973d..c7f8ba441b92e2c093aa7751671d4ecb3c1de7c5 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/submission/SubmissionApiService.java @@ -101,8 +101,7 @@ public class SubmissionApiService implements SubmissionService { @Override public Submission sendSubmission(final SubmitSubmission submission) { - final Map<String, Object> params = new HashMap<>(); - params.put("submissionId", submission.getSubmissionId()); + final Map<String, Object> params = Map.of("submissionId", submission.getSubmissionId()); final RequestSettings requestSettings = RequestSettings.builder() .url(config.getSubmissionEndpoint()) .method(HttpMethod.PUT) @@ -139,8 +138,7 @@ public class SubmissionApiService implements SubmissionService { @Override public Submission getSubmission(final UUID submissionId) { - final Map<String, Object> params = new HashMap<>(); - params.put("submissionId", submissionId); + final Map<String, Object> params = Map.of("submissionId", submissionId); final RequestSettings requestSettings = RequestSettings.builder() .url(config.getSubmissionEndpoint()) .method(HttpMethod.GET) diff --git a/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java b/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java index f2d50ef11cb3e7fe1fa4687ca4815f3d75d133ab..7c1eb4a47e6dc5da9c769dc2c2921c5f8810be07 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/util/StopWatch.java @@ -5,7 +5,8 @@ package dev.fitko.fitconnect.core.util; */ public final class StopWatch { - private StopWatch(){} + private StopWatch() { + } /** * Get current system time in ms. @@ -20,14 +21,13 @@ public final class StopWatch { * Formats end time based on start time (end - start) in a readable format (e.g. sec:ms) * * @param startTime start time of the measured call - * * @return formatted elapsed time since start */ - public static String stopWithFormattedTime(final long startTime){ + public static String stopWithFormattedTime(final long startTime) { return Formatter.formatMillis(stop(startTime)); } - private static long stop(final long startTime){ + private static long stop(final long startTime) { return System.currentTimeMillis() - startTime; } } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java b/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java index 9a722f69143763dd7488bdb0ee2290ce3d87025d..c422a8f36da209601d4479ace25d8e0670a73ef0 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/util/Strings.java @@ -7,6 +7,7 @@ public final class Strings { /** * Tests a given string on null or empty + * * @param s string to test * @return true if the string is null OR empty, false if both conditions do not apply */ @@ -16,6 +17,7 @@ public final class Strings { /** * Tests a given string on not null and not empty + * * @param s string to test * @return true if the string is not null AND not empty, false if one condition does not apply */ @@ -23,4 +25,14 @@ public final class Strings { return !isNullOrEmpty(s); } + /** + * Replace all non-printable control characters like \t, \r, \n and spaces from the given s. + * + * @param s string that should be cleaned + * @return cleaned string without non-printable chars + */ + public static String cleanNonPrintableChars(final String s) { + return s.replaceAll("\\p{C}", ""); + } + } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java b/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java index 52a322aef04330c0bebaa171fc04df397ded32c2..37455f13cf52a0e8c6b8e053712ae8ce11da4454 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/validation/DefaultValidationService.java @@ -33,6 +33,7 @@ import org.xml.sax.XMLReader; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetSocketAddress; @@ -47,6 +48,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -57,7 +59,9 @@ public class DefaultValidationService implements ValidationService { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultValidationService.class); private static final ObjectMapper MAPPER = new ObjectMapper().setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); - private static final JsonSchemaFactory SCHEMA_FACTORY = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + private static final JsonSchemaFactory SCHEMA_FACTORY_DRAFT_2020 = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + + private static final JsonSchemaFactory SCHEMA_FACTORY_DRAFT_2007 = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); public static final String VALID_SCHEMA_URL_EXPRESSION = "https://schema\\.fitko\\.de/fit-connect/metadata/1\\.\\d+\\.\\d+/metadata.schema.json"; private final MessageDigestService messageDigestService; @@ -96,7 +100,7 @@ public class DefaultValidationService implements ValidationService { final JsonNode inputNode = MAPPER.readTree(setEventPayload); final URI schemaUri = URI.create(inputNode.get(EventClaimFields.CLAIM_SCHEMA).asText()); if (schemaProvider.isAllowedSetSchema(schemaUri)) { - return validateJsonSchema(schemaProvider.loadLatestSetSchema(), inputNode); + return validate2020JsonSchema(schemaProvider.loadLatestSetSchema(), inputNode); } else { return ValidationResult.error(new SchemaNotFoundException("SET payload schema not supported: " + schemaUri)); } @@ -115,7 +119,19 @@ public class DefaultValidationService implements ValidationService { try { final String metadataJson = MAPPER.writeValueAsString(metadata); final JsonNode inputNode = MAPPER.readTree(metadataJson); - return validateJsonSchema(schemaProvider.loadMetadataSchema(config.getMetadataSchemaWriteVersion()), inputNode); + return validate2020JsonSchema(schemaProvider.loadMetadataSchema(config.getMetadataSchemaWriteVersion()), inputNode); + } catch (final JsonProcessingException e) { + return ValidationResult.error(e); + } + } + + @Override + public ValidationResult validateDestinationSchema(final Map<String, Object> destinationPayload) { + try { + final String destinationPayloadJson = MAPPER.writeValueAsString(destinationPayload); + final JsonNode inputNode = MAPPER.readTree(destinationPayloadJson); + final String schema = schemaProvider.loadDestinationSchema(config.getDestinationSchemaVersion()); + return returnValidationResult(SCHEMA_FACTORY_DRAFT_2007.getSchema(schema).validate(inputNode)); } catch (final JsonProcessingException e) { return ValidationResult.error(e); } @@ -157,25 +173,25 @@ public class DefaultValidationService implements ValidationService { } @Override - public ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret) { + public ValidationResult validateCallback(final String hmac, final Long timestamp, final String httpBody, final String callbackSecret) { - ZonedDateTime providedTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()); - ZonedDateTime currentTimeFiveMinutesAgo = ZonedDateTime.now().minusMinutes(5); + final ZonedDateTime providedTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()); + final ZonedDateTime currentTimeFiveMinutesAgo = ZonedDateTime.now().minusMinutes(5); if (providedTime.isBefore(currentTimeFiveMinutesAgo)) { return ValidationResult.error(new ValidationException("Timestamp provided by callback is expired.")); } - String expectedHmac = this.messageDigestService.calculateHMAC(timestamp + "." + httpBody, callbackSecret); + final String expectedHmac = messageDigestService.calculateHMAC(timestamp + "." + httpBody, callbackSecret); if (!hmac.equals(expectedHmac)) { - return ValidationResult.error(new ValidationException("HMAC provided by callback does not match the expected result.")); + return ValidationResult.error(new ValidationException("HMAC provided by callback does not match the expected result.")); } return ValidationResult.ok(); } - private ValidationResult validateJsonSchema(final String schema, final JsonNode inputNode) { - return returnValidationResult(SCHEMA_FACTORY.getSchema(schema).validate(inputNode)); + private ValidationResult validate2020JsonSchema(final String schema, final JsonNode inputNode) { + return returnValidationResult(SCHEMA_FACTORY_DRAFT_2020.getSchema(schema).validate(inputNode)); } private void validateKey(final RSAKey publicKey, final KeyOperation purpose) throws JWKValidationException, CertificateEncodingException { diff --git a/core/src/main/resources/destination-schema/destination_schema.json b/core/src/main/resources/destination-schema/destination_schema.json new file mode 100644 index 0000000000000000000000000000000000000000..db8a5dcd9e751f5f2206b7e854fc4446139e00da --- /dev/null +++ b/core/src/main/resources/destination-schema/destination_schema.json @@ -0,0 +1,150 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://schema.fitko.de/fit-connect/xzufi/destination.schema.json", + "type": "object", + "title": "FIT-Connect Adressierungsinformationen", + "description": "Payload der signierten Adressierungsinformationen", + "examples": [ + { + "submissionHost": "submission-api-testing.fit-connect.fitko.dev", + "iss": "https://portal.auth-testing.fit-connect.fitko.dev", + "services": [ + { + "gebietIDs": [ + "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345" + ], + "leistungIDs": [ + "urn:de:fim:leika:leistung:100" + ] + } + ], + "destinationId": "0c438057-ec3e-4ce0-b154-1683b5d3c2e8", + "iat": 1637685592, + "jti": "ae37e58b-e280-4706-b99e-738f24c8d98f" + } + ], + "required": [ + "submissionHost", + "iss", + "services", + "destinationId", + "iat", + "jti" + ], + "properties": { + "submissionHost": { + "type": "string", + "title": "Submission-Host", + "description": "Technischer Bezeichner für die Host-Adresse des für eine bestimmte Destination-ID zuständigen Zustelldienstes.", + "examples": [ + "submission-api-testing.fit-connect.fitko.dev" + ] + }, + "iss": { + "type": "string", + "title": "Issuer des JWT", + "description": "URL des Self-Service-Portals", + "examples": [ + "https://portal.auth-testing.fit-connect.fitko.dev" + ] + }, + "services": { + "type": "array", + "title": "Verwaltungsleistungen", + "description": "Die Liste der vom Zustellpunkt angebotenen Verwaltungsleistungen und geographischen Zuordnungen (siehe gebietIDs und leistungIDs)", + "examples": [ + [ + { + "gebietIDs": [ + "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345" + ], + "leistungIDs": [ + "urn:de:fim:leika:leistung:100" + ] + } + ] + ], + "items": { + "type": "object", + "title": "Verwaltungsleistung", + "description": "Eine vom Zustellpunkt angebotene Verwaltungsleistung und geographischen Zuordnungen (siehe gebietIDs und leistungIDs)", + "examples": [ + { + "gebietIDs": [ + "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345" + ], + "leistungIDs": [ + "urn:de:fim:leika:leistung:100" + ] + } + ], + "required": [ + "gebietIDs", + "leistungIDs" + ], + "properties": { + "gebietIDs": { + "type": "array", + "title": "gebietIDs", + "description": "Eine Liste der zur Zuständigkeitsabbildung gehörigen Gebiets-IDs", + "examples": [ + [ + "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345" + ] + ], + "items": { + "type": "string", + "title": "gebietID", + "description": "Gebiets-ID als URN", + "examples": [ + "urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs:12345" + ] + } + }, + "leistungIDs": { + "type": "array", + "title": "leistungIDs", + "description": "Eine Liste der zur Zuständigkeitsabbildung gehörigen Leistungs-IDs", + "examples": [ + [ + "urn:de:fim:leika:leistung:100" + ] + ], + "items": { + "type": "string", + "title": "leistungID", + "description": "Leistungs-ID als URN", + "examples": [ + "urn:de:fim:leika:leistung:100" + ] + } + } + } + } + }, + "destinationId": { + "type": "string", + "title": "Destination-ID", + "description": "Die für den Zustellpunkt vergebene ID", + "examples": [ + "0c438057-ec3e-4ce0-b154-1683b5d3c2e8" + ] + }, + "iat": { + "type": "integer", + "title": "Zeitpunkt der Signaturerstellung (issued at)", + "description": "Zeitpunkt der Signaturerstellung als Unix Timestamp", + "examples": [ + 1637685592 + ] + }, + "jti": { + "type": "string", + "title": "JWT Token ID", + "description": "Eindeutige ID des JWT", + "examples": [ + "ae37e58b-e280-4706-b99e-738f24c8d98f" + ] + } + } +} \ No newline at end of file diff --git a/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java b/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java index 47c0b9c9f2fd43743b63abd11715b85bde2b5ceb..7ddaeffe04914a28eeea144648c545bffaf248bd 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/RestEndpointBase.java @@ -30,7 +30,7 @@ public abstract class RestEndpointBase { protected ApplicationConfig getTestConfig(final String fakeBaseUrl) { final var envName = new EnvironmentName("TESTING"); - final var environments = Map.of(envName, new Environment(fakeBaseUrl, fakeBaseUrl,fakeBaseUrl,fakeBaseUrl, false)); + final var environments = Map.of(envName, new Environment(fakeBaseUrl, fakeBaseUrl, fakeBaseUrl, fakeBaseUrl, false)); return ApplicationConfig.builder() .environments(environments) diff --git a/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java b/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java index f2cc9c9d4c9dd5a5217b0fb2028950bd4e4c9588..d4ccb711db8fc3df8e10a5bc368e30e6e0b79ea4 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/events/EventLogVerifierTest.java @@ -445,7 +445,7 @@ class EventLogVerifierTest { when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); - when(keyServiceMock.getSubmissionServiceSignatureKey(any())).thenReturn(rsaKey); + when(keyServiceMock.getSubmissionServicePublicKey(any())).thenReturn(rsaKey); // When final List<ValidationResult> validationResults = underTest.validateEventLogs(ctx, List.of(signedJWT)); diff --git a/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java index 2500d1e32979e5bfd1c52bbad446e22ce29d3805..a790df4634bbef654f6e6d7eefda5a7661df9da7 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/events/SecurityEventTokenServiceTest.java @@ -15,6 +15,7 @@ import dev.fitko.fitconnect.api.domain.model.event.EventPayload; import dev.fitko.fitconnect.api.domain.model.event.Event; import dev.fitko.fitconnect.api.domain.model.event.problems.submission.AttachmentsMismatch; import dev.fitko.fitconnect.api.domain.model.submission.Submission; +import dev.fitko.fitconnect.api.domain.schema.SchemaResources; import dev.fitko.fitconnect.api.exceptions.EventCreationException; import dev.fitko.fitconnect.api.services.crypto.CryptoService; import dev.fitko.fitconnect.api.services.events.SecurityEventService; @@ -53,26 +54,30 @@ class SecurityEventTokenServiceTest { @BeforeEach void setUp() throws IOException, ParseException { final ApplicationConfig config = getApplicationConfig(); - this.cryptoService = new JWECryptoService(new HashService()); - this.signingKey = JWK.parse(getResourceAsString("private_test_signing_key.json")).toRSAKey(); - this.encryptionKey = JWK.parse(getResourceAsString("public_encryption_test_key.json")).toRSAKey(); + cryptoService = new JWECryptoService(new HashService()); + signingKey = JWK.parse(getResourceAsString("private_test_signing_key.json")).toRSAKey(); + encryptionKey = JWK.parse(getResourceAsString("public_encryption_test_key.json")).toRSAKey(); + final List<String> setSchemas = SchemaConfig.getSetSchemaFilePaths("/set-schema"); final List<String> metadataSchemas = SchemaConfig.getMetadataSchemaFileNames("/metadata-schema"); - final SchemaProvider schemaProvider = new SchemaResourceProvider(setSchemas, metadataSchemas); - this.validationService = new DefaultValidationService(config, new HashService(), schemaProvider); - this.underTest = new SecurityEventTokenService(config, this.validationService, this.signingKey); + final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema"); + final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas); + final SchemaProvider schemaProvider = new SchemaResourceProvider(schemaResources); + + validationService = new DefaultValidationService(config, new HashService(), schemaProvider); + underTest = new SecurityEventTokenService(config, validationService, signingKey); } @Test void testAcceptSubmissionEventWithoutProblems() throws JOSEException { // Given - final var encryptedData = this.cryptoService.encryptString(this.encryptionKey, "test data"); - final var encryptedMetadata = this.cryptoService.encryptString(this.encryptionKey, "test metadata"); + final var encryptedData = cryptoService.encryptString(encryptionKey, "test data"); + final var encryptedMetadata = cryptoService.encryptString(encryptionKey, "test metadata"); final var encryptedAttachments = new HashMap<UUID, String>(); - encryptedAttachments.put(UUID.randomUUID(), this.cryptoService.encryptString(this.encryptionKey, "test attachment 1")); - encryptedAttachments.put(UUID.randomUUID(), this.cryptoService.encryptString(this.encryptionKey, "test attachment 2")); + encryptedAttachments.put(UUID.randomUUID(), cryptoService.encryptString(encryptionKey, "test attachment 1")); + encryptedAttachments.put(UUID.randomUUID(), cryptoService.encryptString(encryptionKey, "test attachment 2")); final var submission = new Submission(); submission.setSubmissionId(UUID.randomUUID()); @@ -84,15 +89,15 @@ class SecurityEventTokenServiceTest { final EventPayload eventPayload = new EventPayload(submission, encryptedAttachments); // When - final SignedJWT signedJWT = this.underTest.createAcceptSubmissionEvent(eventPayload); + final SignedJWT signedJWT = underTest.createAcceptSubmissionEvent(eventPayload); // Then final Map<String, Object> payload = signedJWT.getPayload().toJSONObject(); final var eventsClaim = (Map) payload.get("events"); final var eventObject = (Map) eventsClaim.get(Event.ACCEPT.getSchemaUri()); - final JWSVerifier jwsVerifier = new RSASSAVerifier(this.signingKey); + final JWSVerifier jwsVerifier = new RSASSAVerifier(signingKey); - assertEquals(JWSAlgorithm.PS512, this.signingKey.getAlgorithm()); + assertEquals(JWSAlgorithm.PS512, signingKey.getAlgorithm()); assertTrue(signedJWT.verify(jwsVerifier)); assertThat(payload.get("sub"), is("submission:" + eventPayload.getSubmissionId())); assertThat(payload.get("txn"), is("case:" + eventPayload.getCaseId())); @@ -104,11 +109,11 @@ class SecurityEventTokenServiceTest { void testAcceptSubmissionEventWithProblem() throws JOSEException { // Given - final var encryptedData = this.cryptoService.encryptString(this.encryptionKey, "test data"); - final var encryptedMetadata = this.cryptoService.encryptString(this.encryptionKey, "test metadata"); + final var encryptedData = cryptoService.encryptString(encryptionKey, "test data"); + final var encryptedMetadata = cryptoService.encryptString(encryptionKey, "test metadata"); final var encryptedAttachments = new HashMap<UUID, String>(); - encryptedAttachments.put(UUID.randomUUID(), this.cryptoService.encryptString(this.encryptionKey, "test attachment")); + encryptedAttachments.put(UUID.randomUUID(), cryptoService.encryptString(encryptionKey, "test attachment")); final var submission = new Submission(); submission.setSubmissionId(UUID.randomUUID()); @@ -122,15 +127,15 @@ class SecurityEventTokenServiceTest { final EventPayload eventPayload = new EventPayload(submission, encryptedAttachments, List.of(problem)); // When - final SignedJWT signedJWT = this.underTest.createAcceptSubmissionEvent(eventPayload); + final SignedJWT signedJWT = underTest.createAcceptSubmissionEvent(eventPayload); // Then final Map<String, Object> payload = signedJWT.getPayload().toJSONObject(); final var eventsClaim = (Map) payload.get("events"); final var eventObject = (Map) eventsClaim.get(Event.ACCEPT.getSchemaUri()); - final JWSVerifier jwsVerifier = new RSASSAVerifier(this.signingKey); + final JWSVerifier jwsVerifier = new RSASSAVerifier(signingKey); - assertEquals(JWSAlgorithm.PS512, this.signingKey.getAlgorithm()); + assertEquals(JWSAlgorithm.PS512, signingKey.getAlgorithm()); assertTrue(signedJWT.verify(jwsVerifier)); assertThat(payload.get("sub"), is("submission:" + eventPayload.getSubmissionId())); assertThat(payload.get("txn"), is("case:" + eventPayload.getCaseId())); @@ -153,15 +158,15 @@ class SecurityEventTokenServiceTest { final EventPayload eventPayload = new EventPayload(submission, List.of(problem)); // When - final SignedJWT signedJWT = this.underTest.createRejectSubmissionEvent(eventPayload); + final SignedJWT signedJWT = underTest.createRejectSubmissionEvent(eventPayload); // Then final Map<String, Object> payload = signedJWT.getPayload().toJSONObject(); final var eventsClaim = (Map) payload.get("events"); final var eventObject = (Map) eventsClaim.get(Event.REJECT.getSchemaUri()); - final JWSVerifier jwsVerifier = new RSASSAVerifier(this.signingKey); + final JWSVerifier jwsVerifier = new RSASSAVerifier(signingKey); - assertEquals(JWSAlgorithm.PS512, this.signingKey.getAlgorithm()); + assertEquals(JWSAlgorithm.PS512, signingKey.getAlgorithm()); assertTrue(signedJWT.verify(jwsVerifier)); assertThat(payload.get("sub"), is("submission:" + eventPayload.getSubmissionId())); assertThat(payload.get("txn"), is("case:" + eventPayload.getCaseId())); @@ -178,12 +183,12 @@ class SecurityEventTokenServiceTest { submission.setSubmissionId(UUID.randomUUID()); submission.setDestinationId(UUID.randomUUID()); submission.setCaseId(UUID.randomUUID()); - submission.setEncryptedData(this.cryptoService.encryptString(this.encryptionKey, "test data")); + submission.setEncryptedData(cryptoService.encryptString(encryptionKey, "test data")); final EventPayload eventPayload = new EventPayload(submission, Map.of()); // When - final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createAcceptSubmissionEvent(eventPayload)); + final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createAcceptSubmissionEvent(eventPayload)); // Then assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/accept-submission.authenticationTags.metadata: null found")); @@ -197,12 +202,12 @@ class SecurityEventTokenServiceTest { submission.setSubmissionId(UUID.randomUUID()); submission.setDestinationId(UUID.randomUUID()); submission.setCaseId(UUID.randomUUID()); - submission.setEncryptedMetadata(this.cryptoService.encryptString(this.encryptionKey, "test metadata")); + submission.setEncryptedMetadata(cryptoService.encryptString(encryptionKey, "test metadata")); final EventPayload eventPayload = new EventPayload(submission, Map.of()); // When - final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createAcceptSubmissionEvent(eventPayload)); + final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createAcceptSubmissionEvent(eventPayload)); // Then assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/accept-submission.authenticationTags.data: null found")); @@ -220,7 +225,7 @@ class SecurityEventTokenServiceTest { final EventPayload eventPayload = new EventPayload(submission); // When - final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createAcceptSubmissionEvent(eventPayload)); + final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createAcceptSubmissionEvent(eventPayload)); // Then assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/accept-submission.authenticationTags.metadata: null found")); @@ -239,7 +244,7 @@ class SecurityEventTokenServiceTest { final EventPayload eventPayload = new EventPayload(submission); // When - final EventCreationException exception = assertThrows(EventCreationException.class, () -> this.underTest.createRejectSubmissionEvent(eventPayload)); + final EventCreationException exception = assertThrows(EventCreationException.class, () -> underTest.createRejectSubmissionEvent(eventPayload)); // Then assertThat(exception.getCause().getMessage(), containsString("$.events.https://schema.fitko.de/fit-connect/events/reject-submission.problems: is missing but it is required")); diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java index 262d9a2146291ec789a2aa7f488dcc67e8f74d41..5dd93e1bdaacedef13ac6076321832eb07fe3240 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyRestTemplateTest.java @@ -17,28 +17,28 @@ public class ProxyRestTemplateTest { @BeforeEach public void startWireMock() { - this.wireMockServer.start(); + wireMockServer.start(); } @AfterEach public void shutDownWireMock() { - this.wireMockServer.stop(); + wireMockServer.stop(); } @Test void userAgentIsCorrect() { WireMock.configureFor("localhost", 8080); - this.wireMockServer.stubFor(get(urlEqualTo("/test")) + wireMockServer.stubFor(get(urlEqualTo("/test")) .withHeader("User-Agent", equalTo("productName/productVersion (commit:commit;os:" + System.getProperty("os.name") + ")")) .willReturn(ok().withBody("")) ); - BuildInfo buildInfo = new BuildInfo(); + final BuildInfo buildInfo = new BuildInfo(); buildInfo.setProductName("productName"); buildInfo.setProductVersion("productVersion"); buildInfo.setCommit("commit"); - RestTemplate restTemplate = new ProxyConfig(null, 0, buildInfo).proxyRestTemplate(); + final RestTemplate restTemplate = new RestService(null, 0, buildInfo).getRestTemplate(); restTemplate.exchange("http://localhost:8080/test", HttpMethod.GET, null, String.class); diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyConfigTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java similarity index 77% rename from core/src/test/java/dev/fitko/fitconnect/core/http/ProxyConfigTest.java rename to core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java index cc0d2feec72f2d0cf1210c894983749036518fab..1c88e3ffc82bb29104e427f94cf454f74ace4320 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/http/ProxyConfigTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/http/RestServiceTest.java @@ -9,18 +9,18 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.*; -class ProxyConfigTest { +class RestServiceTest { - ProxyConfig underTest; + RestService underTest; @Test void proxyRestTemplate() { // Given - underTest = new ProxyConfig("http://testhost.de", 8080, new BuildInfo()); + underTest = new RestService("http://testhost.de", 8080, new BuildInfo()); // When - final RestTemplate template = underTest.proxyRestTemplate(); + final RestTemplate template = underTest.getRestTemplate(); // Then assertNotNull(template); @@ -32,7 +32,7 @@ class ProxyConfigTest { void hasProxySet() { // Given - underTest = new ProxyConfig("http://testhost.de", 8080, new BuildInfo()); + underTest = new RestService("http://testhost.de", 8080, new BuildInfo()); // When final boolean proxySet = underTest.hasProxySet(); @@ -45,7 +45,7 @@ class ProxyConfigTest { void hasProxyNotSet() { // Given - underTest = new ProxyConfig(null, 0, new BuildInfo()); + underTest = new RestService(null, 0, new BuildInfo()); // When final boolean proxySet = underTest.hasProxySet(); @@ -58,7 +58,7 @@ class ProxyConfigTest { void testToString() { // Given - underTest = new ProxyConfig("http://testhost.de", 8080, new BuildInfo()); + underTest = new RestService("http://testhost.de", 8080, new BuildInfo()); // When final String configAsString = underTest.toString(); diff --git a/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java b/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java index c80fc734980c2ce426bc0dbe158565f5d5daaed6..7a9c0bc41a58172104bad780906bb3c17a503acd 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/http/RestTemplateTest.java @@ -34,7 +34,7 @@ public class RestTemplateTest extends RestEndpointBase { @BeforeEach void setup() { - underTest = new ProxyConfig(null, 0, new BuildInfo()).proxyRestTemplate(); + underTest = new RestService(null, 0, new BuildInfo()).getRestTemplate(); } @Test diff --git a/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java index 49adb82eb0d723ddfdcc6d008eb9132d31506bcf..54b90334da0c9a89fc1ae3323418afa358daa032 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/keys/PublicKeyServiceTest.java @@ -198,7 +198,7 @@ class PublicKeyServiceTest extends RestEndpointBase { when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); // When - final RSAKey retrievedSignatureKey = underTest.getSubmissionServiceSignatureKey("123"); + final RSAKey retrievedSignatureKey = underTest.getSubmissionServicePublicKey("123"); // Then assertThat(retrievedSignatureKey, is(RSAKey.parse(jwkSet.getKeyByKeyId("123").toJSONObject()))); @@ -228,7 +228,37 @@ class PublicKeyServiceTest extends RestEndpointBase { when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); // When - final RSAKey retrievedSignatureKey = underTest.getPortalSignatureKey("123"); + final RSAKey retrievedSignatureKey = underTest.getPortalPublicKey("123"); + + // Then + assertThat(retrievedSignatureKey, is(RSAKey.parse(jwkSet.getKeyByKeyId("123").toJSONObject()))); + } + + @Test + void getSignatureKeyFromCustomUrlTest() throws IOException, ParseException { + + // Given + final Destination destination = new Destination(); + destination.setDestinationId(UUID.randomUUID()); + destination.setEncryptionKid("123"); + + final OAuthToken authToken = new OAuthToken(); + authToken.setAccessToken("abc123"); + + final JWKSet jwkSet = new JWKSet(JWK.parse(getResourceAsString("/public_signature_test_key.json"))); + + wireMockServer.stubFor( + get(urlEqualTo("/custom/path/.well-known/jwks.json")) + .willReturn(ok() + .withBody(new ObjectMapper().writeValueAsString(jwkSet.toJSONObject())) + .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .withStatus(200))); + + when(authServiceMock.getCurrentToken()).thenReturn(authToken); + when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + + // When + final RSAKey retrievedSignatureKey = underTest.getWellKnownKeysForSubmissionUrl("http://localhost:" + wireMockServer.port() + "/custom/path", "123"); // Then assertThat(retrievedSignatureKey, is(RSAKey.parse(jwkSet.getKeyByKeyId("123").toJSONObject()))); diff --git a/core/src/test/java/dev/fitko/fitconnect/core/routing/RouteVerifierTest.java b/core/src/test/java/dev/fitko/fitconnect/core/routing/RouteVerifierTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4b441543955a154e657bbe5b228cfb2d64d78c3e --- /dev/null +++ b/core/src/test/java/dev/fitko/fitconnect/core/routing/RouteVerifierTest.java @@ -0,0 +1,258 @@ +package dev.fitko.fitconnect.core.routing; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.util.Base64URL; +import com.nimbusds.jwt.SignedJWT; +import dev.fitko.fitconnect.api.domain.model.route.Route; +import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.ValidationException; +import dev.fitko.fitconnect.api.services.keys.KeyService; +import dev.fitko.fitconnect.api.services.validation.ValidationService; +import dev.fitko.fitconnect.core.SubmissionSenderTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RouteVerifierTest { + + private RouteVerifier underTest; + private KeyService keyServiceMock; + private ValidationService validatorMock; + private RSAKey portalSignatureKey; + private RSAKey submissionServiceSignatureKey; + + @BeforeEach + void setup() throws IOException, ParseException { + keyServiceMock = mock(KeyService.class); + validatorMock = mock(ValidationService.class); + underTest = new RouteVerifier(keyServiceMock, validatorMock); + portalSignatureKey = RSAKey.parse(getResourceAsString("/portal_well_known_signature_key.json")); + submissionServiceSignatureKey = RSAKey.parse(getResourceAsString("/submission_service_well_known_signature_key.json")); + } + + @Test + void validateRouteDestinationTest() throws IOException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok()); + + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + // Then + assertTrue(validationResult.isValid()); + } + + @Test + void hostUrlDoesNotMatchTest() throws IOException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + route.getDestinationParameters().setSubmissionUrl("https://dev.null.net"); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok()); + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + // Then + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("Submission host does not match destinationParameters submission url submission-api-testing.fit-connect.fitko.dev")); + } + + @Test + void requiredAlgorithmDoesNotMatchTest() throws IOException, ParseException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature()); + + final Base64URL headerWithWrongAlgorithm = new JWSHeader(JWSAlgorithm.ES512).toBase64URL(); + final SignedJWT signatureWithWrongAlg = new SignedJWT(headerWithWrongAlgorithm, signature.getParsedParts()[1], signature.getParsedParts()[2]); + route.setDestinationSignature(signatureWithWrongAlg.serialize()); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + // Then + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("Algorithm in signature header is not PS512")); + } + + @Test + void requiredClaimsNotPresentTest() throws IOException, ParseException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature()); + + final Payload payloadWithMissingClaims = new Payload(Map.of("submissionHost", "submission-api-testing.fit-connect.fitko.dev")); + final SignedJWT signatureWithMissingClaims = new SignedJWT(signature.getHeader().toBase64URL(), payloadWithMissingClaims.toBase64URL(), signature.getParsedParts()[2]); + + route.setDestinationSignature(signatureWithMissingClaims.serialize()); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.error(new ValidationException("Payload missing required claims"))); + + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + // Then + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("Payload missing required claims")); + } + + @Test + void requestedRegionNotSupportedByDestinationTest() throws IOException, ParseException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature()); + + final List<Map<String, List<String>>> serviceClaim = List.of( + Map.of( + "gebietIDs", List.of("1234567"), + "leistungIDs", List.of("urn:de:fim:leika:leistung:99123456760610") + ) + ); + + final Map<String, Object> claims = Map.of( + "submissionHost", "submission-api-testing.fit-connect.fitko.dev", + "iss", "fit-connect", + "iat", new Date().getTime(), + "jti", UUID.randomUUID().toString(), + "destinationId", UUID.randomUUID().toString(), + "services", serviceClaim); + + final Payload payloadWithServicesNotMatching = new Payload(claims); + final SignedJWT signatureWithServices = new SignedJWT(signature.getHeader().toBase64URL(), payloadWithServicesNotMatching.toBase64URL(), signature.getParsedParts()[2]); + + route.setDestinationSignature(signatureWithServices.serialize()); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok()); + + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + // Then + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("Requested region '064350014014' is not supported by the destinations services")); + } + + @Test + void requestedServiceIdentifierNotSupportedByDestinationTest() throws IOException, ParseException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + final SignedJWT signature = SignedJWT.parse(route.getDestinationSignature()); + + final List<Map<String, List<String>>> serviceClaim = List.of( + Map.of( + "gebietIDs", List.of("064350014014"), + "leistungIDs", List.of("urn:de:fim:leika:leistung:1111111111111") + ) + ); + + final Map<String, Object> claims = Map.of( + "submissionHost", "submission-api-testing.fit-connect.fitko.dev", + "iss", "fit-connect", + "iat", new Date().getTime(), + "jti", UUID.randomUUID().toString(), + "destinationId", UUID.randomUUID().toString(), + "services", serviceClaim); + + final Payload payloadWithServicesNotMatching = new Payload(claims); + final SignedJWT signatureWithServices = new SignedJWT(signature.getHeader().toBase64URL(), payloadWithServicesNotMatching.toBase64URL(), signature.getParsedParts()[2]); + + route.setDestinationSignature(signatureWithServices.serialize()); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok()); + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + // Then + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("Requested service identifier 'urn:de:fim:leika:leistung:99123456760610' is not supported by the destinations services")); + } + + @Test + void signatureVerificationFailedTest() throws IOException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + route.getDestinationParameters().setEncryptionKid("Wrong_Key_Id_That_Fails_The_Public_Key_Retrieval"); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok()); + + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + // Then + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("Invalid destination parameter signature for route d40e7b13-da98-4b09-9e16-bbd61ca81510")); + } + + @Test + void combiningDetachedSignatureWithPayloadFailedTest() throws IOException { + + // Given + final Route route = new ObjectMapper().readValue(getResourceAsString("/sample_route.json"), Route.class); + + // replace detached signature payload part with wrong data (..) + route.setDestinationParametersSignature(route.getDestinationParametersSignature().replace("..", "")); + + when(keyServiceMock.getPortalPublicKey(anyString())).thenReturn(portalSignatureKey); + when(keyServiceMock.getWellKnownKeysForSubmissionUrl(anyString(), anyString())).thenReturn(submissionServiceSignatureKey); + when(validatorMock.validateDestinationSchema(anyMap())).thenReturn(ValidationResult.ok()); + + + // When + final ValidationResult validationResult = underTest.validateRouteDestinations(List.of(route), "urn:de:fim:leika:leistung:99123456760610", "064350014014"); + + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("Invalid serialized unsecured/JWS/JWE object: Missing part delimiters")); + } + + private String getResourceAsString(final String filename) throws IOException { + return new String(SubmissionSenderTest.class.getResourceAsStream(filename).readAllBytes()); + } +} \ No newline at end of file diff --git a/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..486a1150b398200e7b3c97319795b39c29b39630 --- /dev/null +++ b/core/src/test/java/dev/fitko/fitconnect/core/routing/RoutingApiServiceTest.java @@ -0,0 +1,123 @@ +package dev.fitko.fitconnect.core.routing; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.fitko.fitconnect.api.config.ApplicationConfig; +import dev.fitko.fitconnect.api.config.Environment; +import dev.fitko.fitconnect.api.config.EnvironmentName; +import dev.fitko.fitconnect.api.domain.model.route.Area; +import dev.fitko.fitconnect.api.domain.model.route.AreaResult; +import dev.fitko.fitconnect.api.domain.model.route.Route; +import dev.fitko.fitconnect.api.domain.model.route.RouteResult; +import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.services.routing.RoutingService; +import dev.fitko.fitconnect.core.RestEndpointBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.*; + +public class RoutingApiServiceTest extends RestEndpointBase { + + private RoutingService underTest; + + @BeforeEach + void setup() { + + final var fakeBaseUrl = "http://localhost:" + wireMockServer.port(); + + final EnvironmentName envName = new EnvironmentName("TESTING"); + final Environment environment = new Environment(); + environment.setRoutingBaseUrl(fakeBaseUrl); + + final ApplicationConfig config = new ApplicationConfig(); + config.setEnvironments(Map.of(envName, environment)); + config.setActiveEnvironment(envName); + + underTest = new RoutingApiService(config, new RestTemplate()); + } + + @Test + void testFindAreas() throws JsonProcessingException { + + // Given + final Area area = new Area(); + area.setName("Berlin"); + + final AreaResult expectedResult = new AreaResult(); + expectedResult.setCount(1); + expectedResult.setOffset(0); + expectedResult.setTotalCount(1); + expectedResult.setAreas(List.of(area)); + + wireMockServer.stubFor( + get(urlEqualTo("/v1/areas?areaSearchexpression=Berlin&offset=0&limit=10")) + .willReturn(ok() + .withBody(new ObjectMapper().writeValueAsString(expectedResult)) + .withHeader("Content-Type", "application/problem+json") + .withStatus(200))); + + // When + final AreaResult areaResult = underTest.getAreas(List.of("Berlin"), 0, 10); + + + // Then + assertNotNull(areaResult); + assertFalse(areaResult.getAreas().isEmpty()); + assertThat(areaResult.getAreas().get(0), is(area)); + } + + @Test + void testFindRoutes() throws JsonProcessingException { + + // Given + final Route route = new Route(); + route.setDestinationId(UUID.randomUUID()); + route.setDestinationName("test destination"); + + final RouteResult expectedResult = new RouteResult(); + expectedResult.setCount(1); + expectedResult.setOffset(0); + expectedResult.setTotalCount(1); + expectedResult.setRoutes(List.of(route)); + + wireMockServer.stubFor( + get(urlEqualTo("/v1/routes?leikaKey=99123456760610&ars=064350014014&offset=0&limit=10")) + .willReturn(ok() + .withBody(new ObjectMapper().writeValueAsString(expectedResult)) + .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + .withStatus(200))); + + // When + final RouteResult routeResult = underTest.getRoutes("99123456760610", "064350014014", null, null, 0, 10); + + // Then + assertNotNull(routeResult); + assertFalse(routeResult.getRoutes().isEmpty()); + assertThat(routeResult.getRoutes().get(0), is(route)); + } + + @Test + void testFindRoutesWithNoSearchCriterion() { + + // Given + final var leikaKey = "99123456760610"; + + // When + final RestApiException exception = assertThrows(RestApiException.class, () -> underTest.getRoutes(leikaKey, null, null, null, 0, 10)); + + // Then + assertThat(exception.getMessage(), containsString(" one search criterion out of ags, ars or areaId must be set")); + + } +} diff --git a/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java b/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java index f4d1fae5007141e16dad971c4fc6350e94b4abdb..0b1899e945d8a6eda17770d21ff9a9ba381ae8e5 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/schema/SchemaResourceProviderTest.java @@ -3,6 +3,7 @@ package dev.fitko.fitconnect.core.schema; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import dev.fitko.fitconnect.api.config.SchemaConfig; +import dev.fitko.fitconnect.api.domain.schema.SchemaResources; import dev.fitko.fitconnect.api.exceptions.SchemaNotFoundException; import dev.fitko.fitconnect.api.services.schema.SchemaProvider; import org.junit.jupiter.api.BeforeEach; @@ -23,29 +24,31 @@ class SchemaResourceProviderTest { void setup() { final List<String> setSchemas = SchemaConfig.getSetSchemaFilePaths("/set-schema"); final List<String> metadataSchemas = SchemaConfig.getMetadataSchemaFileNames("/metadata-schema"); - this.underTest = new SchemaResourceProvider(setSchemas, metadataSchemas); + final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema"); + final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas); + underTest = new SchemaResourceProvider(schemaResources); } @Test void isAllowedSetSchema() { - assertTrue(this.underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_1.getSchemaUri())); - assertTrue(this.underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_0.getSchemaUri())); - assertTrue(this.underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/1.2.3/set-payload.schema.json"))); + assertTrue(underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_1.getSchemaUri())); + assertTrue(underTest.isAllowedSetSchema(SchemaConfig.SET_V_1_0_0.getSchemaUri())); + assertTrue(underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/1.2.3/set-payload.schema.json"))); } @Test void isNoAllowedSetSchema() { - assertFalse(this.underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/2.2.3/set-payload.schema.json"))); + assertFalse(underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/set-payload/2.2.3/set-payload.schema.json"))); } @Test void isAllowedMetadataSchema() { - assertTrue(this.underTest.isAllowedMetadataSchema(SchemaConfig.METADATA_V_1_0_0.getSchemaUri())); + assertTrue(underTest.isAllowedMetadataSchema(SchemaConfig.METADATA_V_1_0_0.getSchemaUri())); } @Test void isNoAllowedMetadataSchema() { - assertFalse(this.underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/metadata/9.9.9/metadata.schema.json"))); + assertFalse(underTest.isAllowedSetSchema(URI.create("https://schema.fitko.de/fit-connect/metadata/9.9.9/metadata.schema.json"))); } @Test @@ -54,7 +57,7 @@ class SchemaResourceProviderTest { final URI schemaUri = SchemaConfig.SET_V_1_0_1.getSchemaUri(); // When - final String setSchema = this.underTest.loadLatestSetSchema(); + final String setSchema = underTest.loadLatestSetSchema(); //Then assertThat(new ObjectMapper().readTree(setSchema).get("$id").asText(), equalTo(schemaUri.toString())); @@ -66,7 +69,19 @@ class SchemaResourceProviderTest { final URI schemaUri = SchemaConfig.METADATA_V_1_0_0.getSchemaUri(); // When - final String metadataSchema = this.underTest.loadMetadataSchema(schemaUri); + final String metadataSchema = underTest.loadMetadataSchema(schemaUri); + + //Then + assertThat(new ObjectMapper().readTree(metadataSchema).get("$id").asText(), equalTo(schemaUri.toString())); + } + + @Test + void loadDestinationSchema() throws JsonProcessingException { + // Given + final URI schemaUri = SchemaConfig.XZUFI_DESTINATION_SCHEMA.getSchemaUri(); + + // When + final String metadataSchema = underTest.loadDestinationSchema(schemaUri); //Then assertThat(new ObjectMapper().readTree(metadataSchema).get("$id").asText(), equalTo(schemaUri.toString())); @@ -75,6 +90,6 @@ class SchemaResourceProviderTest { @Test void loadMetadataSchemaThatDoesNotExist() { final URI schemaUri = URI.create("https://schema.fitko.de/fit-connect/metadata/9.9.9/metadata.schema.json"); - assertThrows(SchemaNotFoundException.class, () -> this.underTest.loadMetadataSchema(schemaUri)); + assertThrows(SchemaNotFoundException.class, () -> underTest.loadMetadataSchema(schemaUri)); } } \ No newline at end of file diff --git a/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java index 16aecff189934eda4632c84d4dff2b244ba50b11..ba35c8acd3eb1d12eccb251955f9cf781263488c 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/submission/SubmissionApiServiceTest.java @@ -274,8 +274,10 @@ class SubmissionApiServiceTest extends RestEndpointBase { .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) .withStatus(200))).getResponse(); + // When final Submission submission = underTest.getSubmission(submissionId); + // Then assertNotNull(submission); assertThat(submission.getSubmissionId(), is(expectedSubmission.getSubmissionId())); } diff --git a/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java b/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java index 7de74f80fb56dd95de3235964a58785bc47c0e9f..ba7f6ec3e1f553294408cf421a9218f2ad9f2462 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/validation/DefaultValidationServiceTest.java @@ -19,6 +19,7 @@ import dev.fitko.fitconnect.api.domain.model.metadata.SignatureType; import dev.fitko.fitconnect.api.domain.model.metadata.data.Data; import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType; import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; +import dev.fitko.fitconnect.api.domain.schema.SchemaResources; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; import dev.fitko.fitconnect.api.exceptions.ValidationException; import dev.fitko.fitconnect.api.services.crypto.MessageDigestService; @@ -57,7 +58,9 @@ class DefaultValidationServiceTest { hashService = new HashService(); final List<String> setSchemas = SchemaConfig.getSetSchemaFilePaths("/set-schema"); final List<String> metadataSchemas = SchemaConfig.getMetadataSchemaFileNames("/metadata-schema"); - schemaProvider = new SchemaResourceProvider(setSchemas, metadataSchemas); + final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema"); + final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas); + schemaProvider = new SchemaResourceProvider(schemaResources); underTest = new DefaultValidationService(config, hashService, schemaProvider); } @@ -210,10 +213,10 @@ class DefaultValidationServiceTest { @Test void validateMetadataWithInvalidSchemaAttribute() { - Metadata metadata = new Metadata(); + final Metadata metadata = new Metadata(); metadata.setSchema("https://schema.fitko.de/fit-connect/metadata/2.0.0/metadata.schema.json"); - ValidationResult validationResult = this.underTest.validateMetadataSchema(metadata); + final ValidationResult validationResult = underTest.validateMetadataSchema(metadata); assertTrue(validationResult.hasError()); assertThat(validationResult.getError().getClass(), equalTo(ValidationException.class)); @@ -363,6 +366,45 @@ class DefaultValidationServiceTest { assertFalse(validationResult.hasError()); } + @Test + void testValidDestinationPayload() { + + // Given + final Map<String,Object> claims = Map.of( + "iss", "submission-service", + "jti", UUID.randomUUID().toString(), + "iat", new Date().getTime(), + "destinationId", UUID.randomUUID().toString(), + "submissionHost", "host", + "services", Collections.emptyList() + ); + + // When + final ValidationResult validationResult = underTest.validateDestinationSchema(claims); + + // Then + assertTrue(validationResult.isValid()); + } + + @Test + void testDestinationPayloadIsMissingMandatoryClaims() { + + // Given + final Map<String,Object> claims = Map.of("test", "claim"); + + // When + final ValidationResult validationResult = underTest.validateDestinationSchema(claims); + + // Then + assertTrue(validationResult.hasError()); + assertThat(validationResult.getError().getMessage(), containsString("jti: is missing but it is required")); + assertThat(validationResult.getError().getMessage(), containsString("iss: is missing but it is required")); + assertThat(validationResult.getError().getMessage(), containsString("iat: is missing but it is required")); + assertThat(validationResult.getError().getMessage(), containsString("services: is missing but it is required")); + assertThat(validationResult.getError().getMessage(), containsString("submissionHost: is missing but it is required")); + assertThat(validationResult.getError().getMessage(), containsString("destinationId: is missing but it is required")); + } + @Test void testValidJson() throws IOException { @@ -395,13 +437,13 @@ class DefaultValidationServiceTest { @Test void validateCallback() { - MessageDigestService mockedMessageDigestService = mock(MessageDigestService.class); + final MessageDigestService mockedMessageDigestService = mock(MessageDigestService.class); when(mockedMessageDigestService.calculateHMAC(anyString(), anyString())).thenReturn("valid"); - DefaultValidationService defaultValidationService = new DefaultValidationService( + final DefaultValidationService defaultValidationService = new DefaultValidationService( new ApplicationConfig(), mockedMessageDigestService, mock(SchemaProvider.class)); - ValidationResult validationResult = defaultValidationService.validateCallback( + final ValidationResult validationResult = defaultValidationService.validateCallback( "valid", ZonedDateTime.now().toInstant().toEpochMilli(), "body", "secret"); verify(mockedMessageDigestService, times(1)).calculateHMAC(anyString(), anyString()); @@ -412,7 +454,7 @@ class DefaultValidationServiceTest { @Test void validateCallbackWithExpiredTimestamp() { - ValidationResult validationResult = this.underTest.validateCallback("", 0L, "", ""); + final ValidationResult validationResult = underTest.validateCallback("", 0L, "", ""); assertFalse(validationResult.isValid()); assertTrue(validationResult.hasError()); @@ -422,7 +464,7 @@ class DefaultValidationServiceTest { @Test void validateCallbackWithInvalidHmac() { - ValidationResult validationResult = this.underTest.validateCallback( + final ValidationResult validationResult = underTest.validateCallback( "invalid", ZonedDateTime.now().toInstant().toEpochMilli(), "body", "secret"); assertFalse(validationResult.isValid()); diff --git a/core/src/test/resources/portal_well_known_signature_key.json b/core/src/test/resources/portal_well_known_signature_key.json new file mode 100644 index 0000000000000000000000000000000000000000..b273876e27c055a48b994743a868fb5bc36d0437 --- /dev/null +++ b/core/src/test/resources/portal_well_known_signature_key.json @@ -0,0 +1,11 @@ +{ + "kty": "RSA", + "e": "AQAB", + "kid": "aeBUhQS8uaJvtzMcTyiEAN3KW4m65uDmL0X1AAIqdCE", + "key_ops": [ + "verify" + ], + "alg": "PS512", + "n": "4Y0sJhadfrQnNZXeS7Pqh73FvtFPXLvLw11h7OiZM0DlqvRNgoYHO5k-kxJKOVCaFek0LjKM1_VQxMVpdChCkHeapdTg60oQTQZj3pG0boR3LStbqN3hNEx_JZC4aHH16kau0vqBBPiOOoq-ExUz-hXz_GMLsp9QVqIkw9okO_tzNPjQOo--GM8r4eSsKzgSHZzmepc9Gfk16eraGicBevlkclk32TmWIE_ErD31dtVbBlK-7GG2NUe-o_5rkiCJ2EwKRHZlLkBYJkkj_IjeUdKc4dawXoE8L83DSBPyapX47_L1VHTnT0hJdOVe6WHtvzzpusZ0Au-YDhp6LSwXnU9d0-VzBJmQvtrep1FM0d9aQrz0e0lVf8wCn13VdKO_FBZw9D7i0XRhF8JqQRblqhcCY7UGshbTTM8HORMFONHFmSQm10qfV29PLmztOhIuubMyYe1DPnlfRkpn5jnt8IPoopl6MliDKSc3m4dgG23KylBpTLr3U-XGQrTlerjrYh4t1LXiJ-jQhLefkak_WnExZJSXv601BgmbGj3GdIhS6lxdMX62cOuwKLVISOmHHxvimpQwhtYwiFR9OmGoKVgtCQ5eMKLwGWVwXSvUJ5YXH-yUyNW1_vOrt0DAtYmXwS_Ij0bMg9WoXKJ-5NtQpnnIzw1lr5bW5fNn2TgWpHk" +} + diff --git a/core/src/test/resources/sample_route.json b/core/src/test/resources/sample_route.json new file mode 100644 index 0000000000000000000000000000000000000000..ba509741a65ef6b606a4cd2026398924a8effefd --- /dev/null +++ b/core/src/test/resources/sample_route.json @@ -0,0 +1,46 @@ +{ + "destinationId": "d40e7b13-da98-4b09-9e16-bbd61ca81510", + "destinationSignature": "eyJraWQiOiJhZUJVaFFTOHVhSnZ0ek1jVHlpRUFOM0tXNG02NXVEbUwwWDFBQUlxZENFIiwidHlwIjoiSldUIiwiYWxnIjoiUFM1MTIifQ.eyJzdWJtaXNzaW9uSG9zdCI6InN1Ym1pc3Npb24tYXBpLXRlc3RpbmcuZml0LWNvbm5lY3QuZml0a28uZGV2IiwiaXNzIjoiaHR0cHM6XC9cL3BvcnRhbC5hdXRoLXRlc3RpbmcuZml0LWNvbm5lY3QuZml0a28uZGV2Iiwic2VydmljZXMiOlt7ImdlYmlldElEcyI6WyJ1cm46ZGU6YnVuZDpkZXN0YXRpczpiZXZvZWxrZXJ1bmdzc3RhdGlzdGlrOnNjaGx1ZXNzZWw6cnM6MDY0MzUwMDE0MDE0Il0sImxlaXN0dW5nSURzIjpbInVybjpkZTpmaW06bGVpa2E6bGVpc3R1bmc6OTkxMjM0NTY3NjA2MTAiXX1dLCJkZXN0aW5hdGlvbklkIjoiZDQwZTdiMTMtZGE5OC00YjA5LTllMTYtYmJkNjFjYTgxNTEwIiwiaWF0IjoxNjUyMjkxMzkwLCJqdGkiOiJkOGI1NTM2NS1mNDMzLTRiNjMtYjg3Yi0xZWRiNTY3YzlmYWMifQ.RSdgZWBwPvsnlCajXF3Rh8uPDEKkAwgxNbzuO5HWCaAKroHQ8NQtoDGHe2iXFULR8ML7Ca5aHmGKR34CgGmdpxitbzDn_rjHe2WModuRclu8n_eEmGhMTAkcH1aQ8pcDnQAcfI44KPqQZ_D8X6IGqxOEMtoYiETay8OAN3Vzk1Ew9n4vrvK5r5eWIx-nu5uMtHhMqT7xg09jH0Ma4owfCiOrobEdM5fz9a5sWoi0aBTufEMR9ai-SuDR1ibNmwD7s7wYCdqCgaOtj-_dNbPNOYVUdHdRKMvPuFYEx8rs32mehgEEHX649m8QN0FAHsuXdPeagFv7ndceH2vrPPenKk6fz6W68fGZhfs6MzaoUBaz2p3uZJdTCk5gPrJu46TBW0uPEI8dY8Aw_SNMGWsyfviAzjbuhJ3JBoKk8tjPjkERWF9hovlrXu9l9s-jbAn-U-PYWsLYNAaCqvMEGLNmyTnsEMQuKWCQDr1mi0K8QqPiPNDe4NHmy49ObcesLB64LIE5did6XVUGOu3qOACg-s9dN0tFmqEw_-CWOhiWoAQRcu4uyzjCUWKoa6zpw_G3I5STzV328bwHf4hBDnTzZ8PH9OwG0vwqX1jeGdqYz3fWEz8gdU_N18t10393BYJy1tEggCnZzIVBqjQXAjV4pcI6G19D-xT3ITonxdMTAZw", + "destinationParameters": { + "encryptionKid": "1e95f036-ccff-425c-a0de-3d89d5cc59fa", + "metadataVersions": [ + "1.0.0" + ], + "publicKeys": { + "keys": [ + { + "kty": "RSA", + "key_ops": [ + "wrapKey" + ], + "alg": "RSA-OAEP-256", + "x5c": [ + "MIIE6jCCAtKgAwIBAgIGAXo1pG0GMA0GCSqGSIb3DQEBCwUAMDYxNDAyBgNVBAMMK2FaV0ptMG1XRmFxRGFOTkJOaDNabVluVmw3eG52SVMxVEJ5SHhaVnNUNm8wHhcNMjEwNjIyMjEzMzI2WhcNMjIwNDE4MjEzMzI2WjA2MTQwMgYDVQQDDCthWldKbTBtV0ZhcURhTk5CTmgzWm1ZblZsN3hudklTMVRCeUh4WlZzVDZvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2ch1Ir3/Lyb9/HxW9RqIodxi9fXhix6APKwqiSfi+JlRqVa1FoAFsW1nW0IbQjkW6sNkWUFWuA9AfVoKT9nnIcnLSjSQ84SI+if6qTbornyKvBjXg8BSecSUUPYyT0+4NmxrXMGHPYbJV7fQq6jPXzkWC5P5jqQ7ObraQp752BcE/JVQUmFk1ydhhbnroHpGUkA+8jG/kiVL+lAz7uUmZCh6i3ZJD5HN1JOE5LMyzUQOgOFUUPiviBywQAbPQuDLydZ2diO5wqm4mwBadAAzC27OllSkNXSgnd9MVajXmtBVpz2ksMaSCAbfB4rK9q5jXd5YMwu1ZlA+ZuWKYm/p1GjbZdx4xk9w23Zkgnrr3SvWnW98686fd03MG1ACAGatq5FcAvGp8BXCKwz5FpyYtOONx+tECYHcHhx/SafOe9siLYObLmBSsLF3TAjigZjpGOuEjBtKyv5OwJj+6YfIYYjlofuqv6GHUGDvv8iQsy6U4eHCoRpKJzmX6L22MUQgisYvQdGY2jbdEni3g/MpciMIbnZFLENVrqHXYcgHN+SbXl/GVR5b3F0ompES55xA7fuYlt4lp5j0IUo0OWM2/tYYHtASZicVAwnbzLQZEA0u+wXZr0ByMWE07Od/KaLUomlBPi1Ac/FU3KOx0APKJUm7D3//aiLZll3Sh9EnIvECAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAFi24JFEmYL2sOqaaQOMHCVm+WQ9QyN/rkvjHLG8th5TIF8NJ4uwxl+n6GBO2uOZOBixHbU5pK9slyt9Xryw7CbL/hhRGSs3GSFR2hkxoBHdzfFnAwegnd1H5XHQwSgh43jGOn9AxsyISpkvFo7+DObh9Cv8NsjpL57No4UJ62XggfYJW43u4+I/fHcDwnvIN0dvnYpQkCbTmrjLa5IkGim9BynfW1k6VuxLya1SsyjBHWw2YNQ4xBJI4OzXhL6OmBSrohF1RbIKOpjtqGfkXZpxufLNV2CnL36r/41c1nop6cmCIMDtnFEQdAmGe8m/8wvVpLnks59C02/WotlK3iORHCYB6G0pHMKFB4zOVANYtLFgqTgN4HNciV3FN0TvI19qzjkAdcB+m+L+LdseIzcQ/BToGyPvWkJ1mvJZIp0ejnlMWIl3VlNpMKeZ7lJbPpZvABO00lK+FynhITtb6N29toE+7JgHAlWmxw6PFFY1x+3xTHBTOU0oUR/TyKsEU0+bNSb/0S+ZyodmnIFbgYWarjK5pUwfTRyPyeVEukg1Gf30c/7f/5KZ/dpLFUNBb/YTNIzYEhGNUyLJ1mrSz33gr4MtvI4uSu0Jpr1NrwdMGvFhr5QOCULuoC9KlokusUpi0GTH0gK3K/TUi6qvU+Wztfa7mqah17BVVFT1wATs=" + ], + "kid": "1e95f036-ccff-425c-a0de-3d89d5cc59fa", + "n": "2ch1Ir3_Lyb9_HxW9RqIodxi9fXhix6APKwqiSfi-JlRqVa1FoAFsW1nW0IbQjkW6sNkWUFWuA9AfVoKT9nnIcnLSjSQ84SI-if6qTbornyKvBjXg8BSecSUUPYyT0-4NmxrXMGHPYbJV7fQq6jPXzkWC5P5jqQ7ObraQp752BcE_JVQUmFk1ydhhbnroHpGUkA-8jG_kiVL-lAz7uUmZCh6i3ZJD5HN1JOE5LMyzUQOgOFUUPiviBywQAbPQuDLydZ2diO5wqm4mwBadAAzC27OllSkNXSgnd9MVajXmtBVpz2ksMaSCAbfB4rK9q5jXd5YMwu1ZlA-ZuWKYm_p1GjbZdx4xk9w23Zkgnrr3SvWnW98686fd03MG1ACAGatq5FcAvGp8BXCKwz5FpyYtOONx-tECYHcHhx_SafOe9siLYObLmBSsLF3TAjigZjpGOuEjBtKyv5OwJj-6YfIYYjlofuqv6GHUGDvv8iQsy6U4eHCoRpKJzmX6L22MUQgisYvQdGY2jbdEni3g_MpciMIbnZFLENVrqHXYcgHN-SbXl_GVR5b3F0ompES55xA7fuYlt4lp5j0IUo0OWM2_tYYHtASZicVAwnbzLQZEA0u-wXZr0ByMWE07Od_KaLUomlBPi1Ac_FU3KOx0APKJUm7D3__aiLZll3Sh9EnIvE", + "e": "AQAB" + } + ] + }, + "replyChannels": { + "deMail": null, + "elster": null, + "eMail": { + "usePgp": true + }, + "fink": null + }, + "status": "active", + "submissionSchemas": [ + { + "schemaUri": "https://schema.fitko.de/fim/s06000178_0.4.schema.json", + "mimeType": "application/json" + } + ], + "submissionUrl": "https://submission-api-testing.fit-connect.fitko.dev" + }, + "destinationParametersSignature": "eyJraWQiOiIzMjg1ODE0Ny1mMDkwLTQzYTktYjJmZC1kMjZhZTViNDFjMDMiLCJjdHkiOiJhcHBsaWNhdGlvblwvam9zZSIsInR5cCI6IkpPU0UiLCJhbGciOiJQUzUxMiJ9..uzPUSfF-SRfVmES8G7uNWW199sqbVElrrGmf8THd4BzavAHrw00yrWiUNpXcHT6KsZS5y4tW8gx6oBlyhlleb9SONygHy2N6ooknHnYwfowPmqk2xsRNn-sU1TPOuGsIgcPEirnp7jn1xa9-LMxA2Pbao8PAGVOtNw7Hkh4xbBrh3sI_6A2Yxg-s3puekBnH3j19eu5-i3otiG9hEQb70D2d-yyNfzCF4-kqFk2uuR8PihjBD9gfqtPDd7xYZ-htyd6jqMVOO0CI2ryiN6SDd-rybw0HK63EfOop4cCoCmk8FXJH9UywoRNRK90BKUtlqBYWR7byZ30qk_YZrovN7yGKLEc9Jpc-YXsx17MDuU76G8dv_YaN4d8Vo82Pqd3r7l_NdLPQEdMGYJMtIL70APjUHV9SN131NhX3MTH_aeNzduifZy3HZmFETtnl6Ly_pEiYGwAYYh3CBXut6VxRKu2g9MQ60X-k39IoVULcOEt7NwpGd5bHaj4EtRn5u8CrY9A0-6kOWFSjij3nlDmcSk4zCOVRf5OddONnlFk-AqxI9i1hqtlJWfDrP3tNTowSzy-ysXEn00H4xrpZfXofZSPpEASsSvYH4GudNpoup32tWfUwrfCLbNrdiMCkvjZ0rFaTeq5IBtWvF7txllfP2MA-Dh2NRf0uK98tzJouado", + "destinationName": "OEID_TC_010_OE01_99 Name Titel ", + "destinationLogo": "https://address.local/logo" +} \ No newline at end of file diff --git a/core/src/test/resources/submission_service_well_known_signature_key.json b/core/src/test/resources/submission_service_well_known_signature_key.json new file mode 100644 index 0000000000000000000000000000000000000000..7bd895f4fb923b7b52c86f1829613bf4a2db8666 --- /dev/null +++ b/core/src/test/resources/submission_service_well_known_signature_key.json @@ -0,0 +1,10 @@ +{ + "kty": "RSA", + "e": "AQAB", + "kid": "32858147-f090-43a9-b2fd-d26ae5b41c03", + "key_ops": [ + "verify" + ], + "alg": "PS512", + "n": "3UltHnOKF38UbpfQ-sG6_p-_SiWTzDohXE1PT6I5xpnQncczwyAF8j-PZv8TxxtvuVxYVJdt610B3bMCwPaD1Kw2ht1mS_Iu2d_SqTxhCDF4S0yrCovo6fKVQXG-IPsbs59-1lefUqjrUbkxRRxH335p22Lmg9Wf0XBJqMWsMBNnul8lKjLP7krk9TKQKR35js42oIliEakByWpq6kzPPlgV4mkFlivnseQaLxItFMh6rs5lLS1kHdrYfwCWS97wf5TO2ubygo_617qKAeN3e6mYYb6k_30WYwnH-vc1_gMf8JuBOZ7sO-OpW09XrQLBkUXTHbIcmLExpm3yOizc-UI9NrLgwomPyg0Ml1n3bKpIWq6dIpyO2LJ6euu8CzPDs2NNv12Z_FUuJMPQWvt-nv3g6AgOHPJZQJ8TjI27yPgjFwtef91OT1jQ_IzgCVp7EFNn-rZtgxhV2PLmhXick8jLszodEgcki5Ooj5oBbze0yV7zPZ3cdXle_NwDKlxkwbAs3WCyTY7mOVA0avsW9cDDwplQnBMDbwKSGOqjDHVI2EY2SN4ur0lYp-gm5IxtTMm2d4CjAXh4XVHBvNNLxBJb9byDA7qM1QgJa1DaKdV5yoTL2VNTWUBYbK4ag7K6avACM7KjL9YvzUIYNnegu5qFtUVXfjBlE_5wNbYzBTk" +} \ No newline at end of file