diff --git a/README.md b/README.md index 6e907988589a1f031fd27e97b1c8313b10c8fc4b..494476882a9fbd458a96d64bf8d9e7331657c118 100644 --- a/README.md +++ b/README.md @@ -512,7 +512,6 @@ var submissionBaseUrl = "https://submission-api-testing.fit-connect.fitko.dev"; ``` ## Roadmap - - [ ] Add auto-reject on technical errors - [ ] Maven central release of 1.0.0-beta 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 5c57e4aa7fa47ba9f2d40b0697ab5c34537e91d9..1b4755b4378e0ecf7c3b71b41dc72b16331338ee 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 @@ -17,13 +17,21 @@ import java.util.stream.Collectors; @AllArgsConstructor public class ApplicationConfig { + @Builder.Default + public static final String AUTH_TAG_SPLIT_TOKEN = "\\."; + @Builder.Default private String httpProxyHost = ""; + @Builder.Default private Integer httpProxyPort = 0; + @Builder.Default private Integer requestTimeoutInSeconds = 30; + @Builder.Default + private boolean enableAutoReject = true; + @Builder.Default private URI setSchemaWriteVersion = SchemaConfig.SET_V_1_0_1.getSchemaUri(); @@ -120,5 +128,9 @@ public class ApplicationConfig { .map(EnvironmentName::getName) .collect(Collectors.joining(" | ")); } + + public boolean isProxySet() { + return (httpProxyPort != null && httpProxyPort > 0) && (httpProxyHost != null || !httpProxyHost.isEmpty()); + } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/EventClaimFields.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/EventClaimFields.java index 8f23d61fe7a5aa3b8dfeaab39f01ef7179e4e690..5218c2cb26f2cea6ab1b1654d8ecdf9213ece729 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/EventClaimFields.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/EventClaimFields.java @@ -9,5 +9,7 @@ public final class EventClaimFields { public static final String CLAIM_SCHEMA = "$schema"; public static final String CLAIM_TXN = "txn"; public static final String CLAIM_SUB = "sub"; + public static final String AUTHENTICATION_TAGS = "authenticationTags"; + public static final String PROBLEMS = "problems"; public static final String HEADER_TYPE = "secevent+jwt"; } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/authtags/AuthenticationTags.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/authtags/AuthenticationTags.java index fd09244fb4cdb267673a111279c8586ccfc22dee..9f7594ead7cf5139888f3dc5d8fcd26d7f9da16c 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/authtags/AuthenticationTags.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/authtags/AuthenticationTags.java @@ -7,7 +7,9 @@ import java.util.UUID; @Data public class AuthenticationTags { + private String metadata; private String data; private Map<UUID, String> attachments; + } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/Problem.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/Problem.java index 2b6313a6ed29f5ab8bc763ecf53f8d2d52d38299..6504666420bd3f791e1b5b86bdb13040e8081213 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/Problem.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/Problem.java @@ -1,6 +1,7 @@ package dev.fitko.fitconnect.api.domain.model.event.problems; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Data; @@ -12,11 +13,19 @@ import lombok.Data; @Data @AllArgsConstructor public class Problem { + @JsonIgnore public static final String SCHEMA_URL = "https://schema.fitko.de/fit-connect/events/problems/"; + @JsonProperty("type") private final String type; + + @JsonProperty("title") private final String title; + + @JsonProperty("detail") private final String detail; + + @JsonProperty("instance") private final String instance; } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionIssue.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionIssue.java index 5408f7eca13945b83a7d325bd537a54c7c130f3f..409e25035c478785111915653bcb03cc8e13f887 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionIssue.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionIssue.java @@ -2,16 +2,18 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.attachment; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import java.util.UUID; + import static java.lang.String.format; -public class AttachmentEncryptionIssue extends Problem { +public final class AttachmentEncryptionIssue extends Problem { - private static final String type = SCHEMA_URL + "encryption-issue"; - private static final String title = "Entschlüsselungs-Fehler"; - private static final String detail = "Die Anlage %s konnte nicht entschlüsselt werden."; - private static final String instance = "attachment:%s"; + private static final String TYPE = SCHEMA_URL + "encryption-issue"; + private static final String TITLE = "Decryption failure"; + private static final String DETAIL = "Decrypting attachment %s failed."; + private static final String INSTANCE = "attachment:%s"; - public AttachmentEncryptionIssue(final String attachmentId) { - super(type, title, format(detail, attachmentId), format(instance, attachmentId)); + public AttachmentEncryptionIssue(final UUID attachmentId) { + super(TYPE, TITLE, format(DETAIL, attachmentId), format(INSTANCE, attachmentId)); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionKeyIssue.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionKeyIssue.java index 1c8ec1d441ad87ceb860929a06a934ffaa6c30fe..00baff8bc62efd823e9846924a2de00a6cbf6ab7 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionKeyIssue.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentEncryptionKeyIssue.java @@ -4,14 +4,18 @@ import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; import static java.lang.String.format; -public class AttachmentEncryptionKeyIssue extends Problem { +public final class AttachmentEncryptionKeyIssue extends Problem { - private static final String type = SCHEMA_URL + "encryption-issue"; - private static final String title = "Entschlüsselungs-Fehler"; - private static final String detail = "Der Schlüssel %s ist nicht der zu diesem Zweck vorgesehene Schlüssel."; - private static final String instance = "attachment:%s"; + private static final String TYPE = SCHEMA_URL + "encryption-issue"; + private static final String TITLE = "Decryption failure"; + private static final String DETAIL = "The key %s is not the key intended for this purpose."; + private static final String INSTANCE = "attachment:%s"; public AttachmentEncryptionKeyIssue(final String keyId) { - super(type, title, format(detail, keyId), format(instance, keyId)); + super(TYPE, TITLE, format(DETAIL, keyId), format(INSTANCE, keyId)); + } + + public static Problem getWithAttachmentId(final String keyId) { + return new AttachmentEncryptionKeyIssue(keyId); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentHashMismatch.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentHashMismatch.java index 744b4155ad374ef2252c0fdacd0c87481eda995f..eafa5058913fa74a1fcb2e2ccaa4e5ac8cc29524 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentHashMismatch.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/AttachmentHashMismatch.java @@ -2,16 +2,18 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.attachment; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import java.util.UUID; + import static java.lang.String.format; -public class AttachmentHashMismatch extends Problem { +public final class AttachmentHashMismatch extends Problem { - private static final String type = SCHEMA_URL + "hash-mismatch"; - private static final String title = "Prüfsumme stimmt nicht"; - private static final String detail = "Der Hash der Anlage %s stimmt nicht."; - private static final String instance = "attachment:%s"; + private static final String TYPE = SCHEMA_URL + "hash-mismatch"; + private static final String TITLE = "Checksum does not match"; + private static final String DETAIL = "Attachment %s hash value is wrong."; + private static final String INSTANCE = "attachment:%s"; - public AttachmentHashMismatch(final String attachmentId) { - super(type, title, format(detail, attachmentId), format(instance, attachmentId)); + public AttachmentHashMismatch(final UUID attachmentId) { + super(TYPE, TITLE, format(DETAIL, attachmentId), format(INSTANCE, attachmentId)); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/IncorrectAttachmentAuthenticationTag.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/IncorrectAttachmentAuthenticationTag.java index f0eefac5fd5fabd163e37f809d8cd398fda69993..68932328be2068faf6a3dd7de58bcc46988ebdad 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/IncorrectAttachmentAuthenticationTag.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/IncorrectAttachmentAuthenticationTag.java @@ -2,16 +2,18 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.attachment; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import java.util.UUID; + import static java.lang.String.format; -public class IncorrectAttachmentAuthenticationTag extends Problem { +public final class IncorrectAttachmentAuthenticationTag extends Problem { - private static final String type = SCHEMA_URL + "incorrect-authentication-tag"; - private static final String title = "Authentication-Tag ungültig"; - private static final String detail = "Das Authentication-Tag der Anlage %s ist ungültig."; - private static final String instance = "attachment:%s"; + private static final String TYPE = SCHEMA_URL + "incorrect-authentication-tag"; + private static final String TITLE = "Authentication tag is invalid"; + private static final String DETAIL = "The authentication tag for the attachment %s is invalid."; + private static final String INSTANCE = "attachment:%s"; - public IncorrectAttachmentAuthenticationTag(final String attachmentId) { - super(type, title, format(detail, attachmentId), format(instance, attachmentId)); + public IncorrectAttachmentAuthenticationTag(final UUID attachmentId) { + super(TYPE, TITLE, format(DETAIL, attachmentId), format(INSTANCE, attachmentId)); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/InvalidAttachmentContent.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/InvalidAttachmentContent.java index 8c81d1886e32f92f81b099988bcdc69082df7020..3436ae96ed0529b1eb82068dbf1ba909e18d848c 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/InvalidAttachmentContent.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/InvalidAttachmentContent.java @@ -2,16 +2,19 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.attachment; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import java.util.UUID; + import static java.lang.String.format; -public class InvalidAttachmentContent extends Problem { +public final class InvalidAttachmentContent extends Problem { - private static final String type = SCHEMA_URL + "invalid-content"; - private static final String title = "Unzulässiger Inhalt"; - private static final String detail = "Der Inhalt der Anlage %s ist nicht zulässig."; - private static final String instance = "attachment:%s"; + private static final String TYPE = SCHEMA_URL + "invalid-content"; + private static final String TITLE = "Invalid content"; + private static final String DETAIL = "The content of the attachment %s is not allowed."; + private static final String INSTANCE = "attachment:%s"; - public InvalidAttachmentContent(final String attachmentId) { - super(type, title, format(detail, attachmentId), format(instance, attachmentId)); + public InvalidAttachmentContent(final UUID attachmentId) { + super(TYPE, TITLE, format(DETAIL, attachmentId), format(INSTANCE, attachmentId)); } + } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/MissingAttachment.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/MissingAttachment.java index e1ed4ab551e5a588157468ff7cea1e54cc333dd4..7afdec4a55a2ce3d792ca41ac773fba8a1ec2451 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/MissingAttachment.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/attachment/MissingAttachment.java @@ -2,15 +2,17 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.attachment; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import java.util.UUID; + import static java.lang.String.format; -public class MissingAttachment extends Problem { - private static final String type = SCHEMA_URL + "missing-attachment"; - private static final String title = "Anlage fehlt"; - private static final String detail = "Die Anlage %s konnte nicht geladen werden."; - private static final String instance = "attachment:%s"; +public final class MissingAttachment extends Problem { + private static final String TYPE = SCHEMA_URL + "missing-attachment"; + private static final String TITLE = "List of attachments is invalid"; + private static final String DETAIL = "Unable to load attachment %s."; + private static final String INSTANCE = "attachment:%s"; - public MissingAttachment(final String attachmentId){ - super(type, title, format(detail, attachmentId), format(instance, attachmentId)); + public MissingAttachment(final UUID attachmentId){ + super(TYPE, TITLE, format(DETAIL, attachmentId), format(INSTANCE, attachmentId)); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionIssue.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionIssue.java index bea6ba5935ff385f147971b75b02578cbf56f8b8..39f5d6d088b5561e8a6c8be3aa5f41da441952c2 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionIssue.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionIssue.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.data; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class DataEncryptionIssue extends Problem { +public final class DataEncryptionIssue extends Problem { - private static final String type = SCHEMA_URL + "encryption-issue"; - private static final String title = "Entschlüsselungs-Fehler"; - private static final String detail = "Der Fachdatensatz konnte nicht entschlüsselt werden."; - private static final String instance = "data"; + private static final String TYPE = SCHEMA_URL + "encryption-issue"; + private static final String TITLE = "Decryption failure"; + private static final String DETAIL = "Decrypting submission data failed."; + private static final String INSTANCE = "data"; public DataEncryptionIssue() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionKeyIssue.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionKeyIssue.java index fd9aad66d0730344e1ef4d6d42b3fe7cac481139..797083f1e01e2f6675c9e4ac69994730d68ef830 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionKeyIssue.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataEncryptionKeyIssue.java @@ -4,14 +4,14 @@ import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; import static java.lang.String.format; -public class DataEncryptionKeyIssue extends Problem { +public final class DataEncryptionKeyIssue extends Problem { - private static final String type = SCHEMA_URL + "encryption-issue"; - private static final String title = "Entschlüsselungs-Fehler"; - private static final String detail = "Der Schlüssel %s ist nicht der zu diesem Zweck vorgesehene Schlüssel."; - private static final String instance = "data"; + private static final String TYPE = SCHEMA_URL + "encryption-issue"; + private static final String TITLE = "Decryption failure"; + private static final String DETAIL = "The key %s is not the key intended for this purpose."; + private static final String INSTANCE = "data"; public DataEncryptionKeyIssue(final String keyId) { - super(type, title, format(detail, keyId), instance); + super(TYPE, TITLE, format(DETAIL, keyId), INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataHashMismatch.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataHashMismatch.java index 34959d7867a2eda8d612334051eeb86658153fbe..657035eefd012ce8fa4484e49a456bb51bdf319b 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataHashMismatch.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataHashMismatch.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.data; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class DataHashMismatch extends Problem { +public final class DataHashMismatch extends Problem { - private static final String type = SCHEMA_URL + "hash-mismatch"; - private static final String title = "Prüfsumme stimmt nicht"; - private static final String detail = "Die Prüfsumme des Fachdatensatzes stimmt nicht."; - private static final String instance = "data"; + private static final String TYPE = SCHEMA_URL + "hash-mismatch"; + private static final String TITLE = "Checksum does not match"; + private static final String DETAIL = "Submission data checksum wrong."; + private static final String INSTANCE = "data"; public DataHashMismatch() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataJsonSyntaxViolation.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataJsonSyntaxViolation.java index 3d3fde4724b0a23f4bec00450e1e2185cca66992..f2f15dd6682116124c0b0838e12563c2c6e9c23a 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataJsonSyntaxViolation.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataJsonSyntaxViolation.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.data; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class DataJsonSyntaxViolation extends Problem { +public final class DataJsonSyntaxViolation extends Problem { - private static final String type = SCHEMA_URL + "syntax-violation"; - private static final String title = "Syntax-Fehler"; - private static final String detail = "Der Fachdatensatz ist kein valides JSON."; - private static final String instance = "data"; + private static final String TYPE = SCHEMA_URL + "syntax-violation"; + private static final String TITLE = "Syntax violation"; + private static final String DETAIL = "Submission data is no valid JSON."; + private static final String INSTANCE = "data"; public DataJsonSyntaxViolation() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataSchemaViolation.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataSchemaViolation.java index 94d6c7613aebffcde6ec72d2064f5b9fdfa0a6b3..31632425e625951f74747cfb0294ffcee4a2b827 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataSchemaViolation.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataSchemaViolation.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.data; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class DataSchemaViolation extends Problem { +public final class DataSchemaViolation extends Problem { - private static final String type = SCHEMA_URL + "schema-violation"; - private static final String title = "Schema-Fehler"; - private static final String detail = "Der Fachdatensatz ist nicht schema-valide."; - private static final String instance = "data"; + private static final String TYPE = SCHEMA_URL + "schema-violation"; + private static final String TITLE = "Schema violation"; + private static final String DETAIL = "Submission data does not comply to schema."; + private static final String INSTANCE = "data"; public DataSchemaViolation() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataXmlSyntaxViolation.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataXmlSyntaxViolation.java index 4098f7dd8b1b76b4749e6af8b76a0d02b560444b..fc12f172786babeab49a91c6f13d73eb85f0b1e5 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataXmlSyntaxViolation.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/DataXmlSyntaxViolation.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.data; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class DataXmlSyntaxViolation extends Problem { +public final class DataXmlSyntaxViolation extends Problem { - private static final String type = SCHEMA_URL + "syntax-violation"; - private static final String title = "Syntax-Fehler"; - private static final String detail = "Der Fachdatensatz ist kein valides XML."; - private static final String instance = "data"; + private static final String TYPE = SCHEMA_URL + "syntax-violation"; + private static final String TITLE = "Syntax violation"; + private static final String DETAIL = "Submission data is no valid XML."; + private static final String INSTANCE = "data"; public DataXmlSyntaxViolation() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/IncorrectDataAuthenticationTag.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/IncorrectDataAuthenticationTag.java index b63523fbcce783d4be34294b0792a833489e754c..463626e522050791211b9d3c52a6778d07939f92 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/IncorrectDataAuthenticationTag.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/data/IncorrectDataAuthenticationTag.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.data; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class IncorrectDataAuthenticationTag extends Problem { +public final class IncorrectDataAuthenticationTag extends Problem { - private static final String type = SCHEMA_URL + "incorrect-authentication-tag"; - private static final String title = "Authentication-Tag ungültig"; - private static final String detail = "Das Authentication-Tag des Fachdatensatzes ist ungültig."; - private static final String instance = "data"; + private static final String TYPE = SCHEMA_URL + "incorrect-authentication-tag"; + private static final String TITLE = "Authentication tag is invalid"; + private static final String DETAIL = "The authentication tag for the submission data is invalid."; + private static final String INSTANCE = "data"; public IncorrectDataAuthenticationTag() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/AttachmentsMismatch.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/AttachmentsMismatch.java index 09aea66ae80f45342c0c212609242db372ef46e1..94f1adfd0f75715a50dc785e86e78553546cd9b8 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/AttachmentsMismatch.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/AttachmentsMismatch.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class AttachmentsMismatch extends Problem { +public final class AttachmentsMismatch extends Problem { - private static final String type = SCHEMA_URL + "attachments-mismatch"; - private static final String title = "Fehlerhafte Anlagen-Liste"; - private static final String detail = "Die Liste der Anlagen in Submission und Metadatensatz stimmt nicht überein."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "attachments-mismatch"; + private static final String TITLE = "List of attachments is invalid"; + private static final String DETAIL = "The list of attachments in the submission and metadata record do not match."; + private static final String INSTANCE = "metadata"; public AttachmentsMismatch() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/IncorrectMetadataAuthenticationTag.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/IncorrectMetadataAuthenticationTag.java index 83227eb987bf5bae4eee561f2fd27e3d0ff52f84..c73d65ea6b656346c2dd9834742d75df647f73e8 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/IncorrectMetadataAuthenticationTag.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/IncorrectMetadataAuthenticationTag.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class IncorrectMetadataAuthenticationTag extends Problem { +public final class IncorrectMetadataAuthenticationTag extends Problem { - private static final String type = SCHEMA_URL + "incorrect-authentication-tag"; - private static final String title = "Authentication-Tag ungültig"; - private static final String detail = "Das Authentication-Tag des Metadatensatzes ist ungültig."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "incorrect-authentication-tag"; + private static final String TITLE = "Authentication tag is invalid"; + private static final String DETAIL = "The authentication tag for the metadata is invalid."; + private static final String INSTANCE = "metadata"; public IncorrectMetadataAuthenticationTag() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionIssue.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionIssue.java index a99b770938551ace51026b7e9bbf34589cb9a6c4..31a0a1dcbe9f67e6942906ea742c09f6726a3b86 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionIssue.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionIssue.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class MetadataEncryptionIssue extends Problem { +public final class MetadataEncryptionIssue extends Problem { - private static final String type = SCHEMA_URL + "encryption-issue"; - private static final String title = "Entschlüsselungs-Fehler"; - private static final String detail = "Die Entschlüsselung des Metadatensatzes ist fehlgeschlagen."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "encryption-issue"; + private static final String TITLE = "Decryption failure"; + private static final String DETAIL = "Decrypting metadata failed."; + private static final String INSTANCE = "metadata"; public MetadataEncryptionIssue() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionKeyIssue.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionKeyIssue.java index 9dcf99d70bd46b8a02e0490d985dc4348ad12820..c7d0791de281a64fd1198bbf5b0b18ce1aec599e 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionKeyIssue.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataEncryptionKeyIssue.java @@ -4,14 +4,14 @@ import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; import static java.lang.String.format; -public class MetadataEncryptionKeyIssue extends Problem { +public final class MetadataEncryptionKeyIssue extends Problem { - private static final String type = SCHEMA_URL + "encryption-issue"; - private static final String title = "Entschlüsselungs-Fehler"; - private static final String detail = "Der Schlüssel %s ist nicht der zu diesem Zweck vorgesehene Schlüssel."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "encryption-issue"; + private static final String TITLE = "Encryption failure"; + private static final String DETAIL = "The key %s is not the key intended for this purpose."; + private static final String INSTANCE = "metadata"; public MetadataEncryptionKeyIssue(final String keyId) { - super(type, title, format(detail, keyId), instance); + super(TYPE, TITLE, format(DETAIL, keyId), INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataJsonSyntaxViolation.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataJsonSyntaxViolation.java index 728470dd28c09b1d694e6f2b18c6ce6849c3c5f9..566839a387cf05e59ea849be042a41fff762fd3e 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataJsonSyntaxViolation.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataJsonSyntaxViolation.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class MetadataJsonSyntaxViolation extends Problem { +public final class MetadataJsonSyntaxViolation extends Problem { - private static final String type = SCHEMA_URL + "syntax-violation"; - private static final String title = "Syntax-Fehler"; - private static final String detail = "Der Metadatensatz ist kein valides JSON."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "syntax-violation"; + private static final String TITLE = "Syntax violation"; + private static final String DETAIL = "Metadata record is no valid JSON."; + private static final String INSTANCE = "metadata"; public MetadataJsonSyntaxViolation() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataSchemaViolation.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataSchemaViolation.java index 4a174f8be375aa60f82671463d2dae47373c0e42..636b572909e3cee712a178edbae042e4b594a626 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataSchemaViolation.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MetadataSchemaViolation.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class MetadataSchemaViolation extends Problem { +public final class MetadataSchemaViolation extends Problem { - private static final String type = SCHEMA_URL + "schema-violation"; - private static final String title = "Schema-Fehler"; - private static final String detail = "Der Metadatensatz ist nicht schema-valide."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "schema-violation"; + private static final String TITLE = "Schema violation"; + private static final String DETAIL = "Submission data does not comply to schema."; + private static final String INSTANCE = "metadata"; public MetadataSchemaViolation() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingData.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingData.java index ba390303a6ecff2003c5ee83a14f01a5a65b39fb..35fda0bcc283b894d441b2444d6fa242b50d2d10 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingData.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingData.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class MissingData extends Problem { +public final class MissingData extends Problem { - private static final String type = SCHEMA_URL + "missing-data"; - private static final String title = "Fachdatensatz fehlt"; - private static final String detail = "Der Fachdatensatz fehlt."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "missing-data"; + private static final String TITLE = "Submission data missing"; + private static final String DETAIL = "Submission data is missing"; + private static final String INSTANCE = "metadata"; public MissingData() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingSchemaReference.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingSchemaReference.java index 17b5ec37589e6ce9853f89e9eef24db43a2afa2f..960a0d232f30d96ed9c0040f1d8b96cd007ffa40 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingSchemaReference.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/MissingSchemaReference.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class MissingSchemaReference extends Problem { +public final class MissingSchemaReference extends Problem { - private static final String type = SCHEMA_URL + "missing-schema"; - private static final String title = "Schema-Referenz fehlt"; - private static final String detail = "Die Schema-Referenz fehlt im Metadatensatz."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "missing-schema"; + private static final String TITLE = "Schema reference missing"; + private static final String DETAIL = "Metadata does not contain a schema reference."; + private static final String INSTANCE = "metadata"; public MissingSchemaReference() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/ServiceMismatch.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/ServiceMismatch.java index 9deae15efd19823f405d7342aefcbbd14960c6ac..046d6356b45c68480a76fc7389cad321db87fddd 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/ServiceMismatch.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/ServiceMismatch.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class ServiceMismatch extends Problem { +public final class ServiceMismatch extends Problem { - private static final String type = SCHEMA_URL + "service-mismatch"; - private static final String title = "Verwaltungsleistung stimmt nicht überein"; - private static final String detail = "Die Verwaltungsleistung in Submission und Metadatensatz stimmen nicht überein."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "service-mismatch"; + private static final String TITLE = "Service type does not match"; + private static final String DETAIL = "Service type of metadata does not match submission."; + private static final String INSTANCE = "metadata"; public ServiceMismatch() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedDataSchema.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedDataSchema.java index ccd19f355db46fad054d3540f3232b182122201f..b4951e47fae96b8ab7b7ef921a65c8f7c37ca60f 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedDataSchema.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedDataSchema.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class UnsupportedDataSchema extends Problem { +public final class UnsupportedDataSchema extends Problem { - private static final String type = SCHEMA_URL + "unsupported-schema"; - private static final String title = "Fachdatenschema nicht unterstützt"; - private static final String detail = "Das angegebene Fachdatenschema wird nicht unterstützt."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "unsupported-schema"; + private static final String TITLE = "Data schema not supported"; + private static final String DETAIL = "Submission data schema is not supported."; + private static final String INSTANCE = "metadata"; public UnsupportedDataSchema() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedMetadataSchema.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedMetadataSchema.java index 29bfc120d398c1f9cd5ac6dbe9ceed3b6d026577..752dd33c476523bdfda20ecf2b939b35983e1250 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedMetadataSchema.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedMetadataSchema.java @@ -4,14 +4,14 @@ import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; import static java.lang.String.format; -public class UnsupportedMetadataSchema extends Problem { +public final class UnsupportedMetadataSchema extends Problem { - private static final String type = SCHEMA_URL + "unsupported-schema"; - private static final String title = "Metadatenschema nicht unterstützt"; - private static final String detail = "Die angegebene Metadatenschema-URI ('%s') ist keines der unterstützten Metadatenschemas."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "unsupported-schema"; + private static final String TITLE = "Metadata schema not supported"; + private static final String DETAIL = "The specified metadata schema URI ('%s') is not referring to a supported metadata schema."; + private static final String INSTANCE = "metadata"; public UnsupportedMetadataSchema(final String metadataSchemaUri) { - super(type, title, format(detail, metadataSchemaUri), instance); + super(TYPE, TITLE, format(DETAIL, metadataSchemaUri), INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedReplyChannel.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedReplyChannel.java index d13246a96340a2658f3ee0487797277b71239632..7eeb1a5d2f4c641f9d79d402e1955653426fabe9 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedReplyChannel.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedReplyChannel.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class UnsupportedReplyChannel extends Problem { +public final class UnsupportedReplyChannel extends Problem { - private static final String type = SCHEMA_URL + "unsupported-reply-channel"; - private static final String title = "Rückkanal nicht unterstützt"; - private static final String detail = "Der gewählte Rückkanal wird nicht unterstützt."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "unsupported-reply-channel"; + private static final String TITLE = "Reply channel is not supported"; + private static final String DETAIL = "The chosen reply channel is not supported."; + private static final String INSTANCE = "metadata"; public UnsupportedReplyChannel() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedService.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedService.java index e7744a14ebf5e3e2a47d14926423303ea0fdcbc9..c6994df90fc010ffe5236cc5b3afa194746e2b51 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedService.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/metadata/UnsupportedService.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.metadata; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class UnsupportedService extends Problem { +public final class UnsupportedService extends Problem { - private static final String type = SCHEMA_URL + "unsupported-service"; - private static final String title = "Verwaltungsleistung nicht unterstützt"; - private static final String detail = "Die angegebene Verwaltungsleistung wird nicht unterstützt."; - private static final String instance = "metadata"; + private static final String TYPE = SCHEMA_URL + "unsupported-service"; + private static final String TITLE = "Service nor supported"; + private static final String DETAIL = "The specified service is not supported."; + private static final String INSTANCE = "metadata"; public UnsupportedService() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/other/TechnicalError.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/other/TechnicalError.java index 344d855d11d659982e87a1ab8a6faaf83e838e05..b947172136a789ec8a181c8400ac3f38c6ed6154 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/other/TechnicalError.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/other/TechnicalError.java @@ -2,14 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.other; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class TechnicalError extends Problem { +public final class TechnicalError extends Problem { - private static final String type = SCHEMA_URL + "technical-error"; - private static final String title = "Technischer Fehler"; - private static final String detail = "Bei der Verarbeitung im empfangenden System trat ein technischer Fehler auf."; - private static final String instance = "other"; + private static final String TYPE = SCHEMA_URL + "technical-error"; + private static final String TITLE = "Technical error"; + private static final String DETAIL = "A technical error occurred during processing in the receiving system."; + private static final String INSTANCE = "other"; public TechnicalError() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/AttachmentsMismatch.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/AttachmentsMismatch.java index 9fc8a745ef9639fe70daf552b4732c3d9c566b58..2c5cccc6c6d7a16e91083419a93c593117297619 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/AttachmentsMismatch.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/AttachmentsMismatch.java @@ -2,13 +2,13 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.submission; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class AttachmentsMismatch extends Problem { - private static final String type = SCHEMA_URL + "attachments-mismatch"; - private static final String title = "Fehlerhafte Anlagen-Liste"; - private static final String detail = "Die Liste der Anlagen in Submission und Event-Log stimmt nicht überein."; - private static final String instance = "submission"; +public final class AttachmentsMismatch extends Problem { + private static final String TYPE = SCHEMA_URL + "attachments-mismatch"; + private static final String TITLE = "List of attachments is invalid"; + private static final String DETAIL = "List of attachments in submission does not match list on event log."; + private static final String INSTANCE = "submission"; public AttachmentsMismatch() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/InvalidEventLog.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/InvalidEventLog.java index 34afda8b034db2911505bd4e2d0a1bb9a0f03a87..94996b1c29da4d0f5ebc893dee2e304867f49d07 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/InvalidEventLog.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/InvalidEventLog.java @@ -2,13 +2,14 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.submission; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class InvalidEventLog extends Problem { - private static final String type = SCHEMA_URL + "invalid-event-log"; - private static final String title = "Inkonsistentes Event-Log"; - private static final String detail = "Das Event-Log ist inkonsistent."; - private static final String instance = "submission"; +public final class InvalidEventLog extends Problem { + + private static final String TYPE = SCHEMA_URL + "invalid-event-log"; + private static final String TITLE = "Inconsistent Event-Log"; + private static final String DETAIL = "The Event-Log is inconsistent."; + private static final String INSTANCE = "submission"; public InvalidEventLog() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/MissingAuthenticationTags.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/MissingAuthenticationTags.java index c63029458d64288e7696291af7cc7a0936b0cc6f..ef1940f737b3e784deb6544bc5b922e5dd0597ac 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/MissingAuthenticationTags.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/MissingAuthenticationTags.java @@ -2,15 +2,13 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.submission; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -import static java.lang.String.format; - public class MissingAuthenticationTags extends Problem { - private static final String type = SCHEMA_URL + "missing-authentication-tags"; - private static final String title = "Fehlende Authentication-Tags"; - private static final String detail = "Das Event 'submit-submission' enthält keine Authentication-Tags."; - private static final String instance = "submission"; + private static final String TYPE = SCHEMA_URL + "missing-authentication-tags"; + private static final String TITLE = "Authentication tags missing"; + private static final String DETAIL = "The 'submit-submission' event does not contain authentication tags."; + private static final String INSTANCE = "submission"; - public MissingAuthenticationTags(final String attachmentId){ - super(type, title, format(detail, attachmentId), format(instance, attachmentId)); + public MissingAuthenticationTags(){ + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/NotExactlyOneSubmitEvent.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/NotExactlyOneSubmitEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..19c4c48bf36338d989407f350f85f49520dcca24 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/NotExactlyOneSubmitEvent.java @@ -0,0 +1,16 @@ +package dev.fitko.fitconnect.api.domain.model.event.problems.submission; + +import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; + +public class NotExactlyOneSubmitEvent extends Problem { + + private static final String TYPE = SCHEMA_URL + "invalid-event-log"; + private static final String TITLE = "Inconsistent Event-Log"; + private static final String DETAIL = "The Event-Log is inconsistent because it does not contain exactly one 'submit' event."; + private static final String INSTANCE = "submission"; + + public NotExactlyOneSubmitEvent() { + super(TYPE, TITLE, DETAIL, INSTANCE); + } + +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/SubmissionTimeout.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/SubmissionTimeout.java index deaa8eb8145e36e99b8e00ec7f26d86d4289dc2a..1d690d87e27e3b4857a55277d369669c37e5786a 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/SubmissionTimeout.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/event/problems/submission/SubmissionTimeout.java @@ -2,13 +2,13 @@ package dev.fitko.fitconnect.api.domain.model.event.problems.submission; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; -public class SubmissionTimeout extends Problem { - private static final String type = SCHEMA_URL + "timeout"; - private static final String title = "Zeitablauf"; - private static final String detail = "Die Einreichung wurde automatisch zurückgewiesen."; - private static final String instance = "submission"; +public final class SubmissionTimeout extends Problem { + private static final String TYPE = SCHEMA_URL + "timeout"; + private static final String TITLE = "Submission Timeout"; + private static final String DETAIL = "The submission was automatically rejected."; + private static final String INSTANCE = "submission"; public SubmissionTimeout() { - super(type, title, detail, instance); + super(TYPE, TITLE, DETAIL, INSTANCE); } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/Metadata.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/Metadata.java index 70fc92285652a6303b550a133af0cf516fba9ab9..ce940361d9e4b4ff200162501262273587bd299e 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/Metadata.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/Metadata.java @@ -5,38 +5,31 @@ import com.fasterxml.jackson.annotation.JsonProperty; import dev.fitko.fitconnect.api.domain.model.metadata.payment.PaymentInformation; import dev.fitko.fitconnect.api.domain.model.replychannel.ReplyChannel; import lombok.AllArgsConstructor; +import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import java.util.List; -@Setter +@Data @EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Metadata { - @Getter @JsonProperty("$schema") private String schema; - @Getter private ContentStructure contentStructure; private PublicServiceType publicServiceType; - @Getter private List<AuthenticationInformation> authenticationInformation; - @Getter private PaymentInformation paymentInformation; - @Getter private ReplyChannel replyChannel; - @Getter private AdditionalReferenceInfo additionalReferenceInfo; } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/AttachmentForValidation.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/AttachmentForValidation.java new file mode 100644 index 0000000000000000000000000000000000000000..8fc92512872a5ad643559b6b0b31030734cdf1a9 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/model/metadata/attachment/AttachmentForValidation.java @@ -0,0 +1,25 @@ +package dev.fitko.fitconnect.api.domain.model.metadata.attachment; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.UUID; + +@Data +@AllArgsConstructor +public class AttachmentForValidation { + + private UUID attachmentId; + private String metadataHash; + private byte[] decryptedData; + private String encryptedData; + private ApiAttachment attachmentMetadata; + + public AttachmentForValidation(final ApiAttachment attachmentMetadata, final String encryptedData, final byte[] decryptedData){ + attachmentId = attachmentMetadata.getAttachmentId(); + metadataHash = attachmentMetadata.getHash().getContent(); + this.attachmentMetadata = attachmentMetadata; + this.decryptedData = decryptedData; + this.encryptedData = encryptedData; + } +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationContext.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationContext.java index 456555ea39494dd7dc190a1d70d4766f80551565..7f59e8f0217aaca96bc107cb26d976e17357de15 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationContext.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationContext.java @@ -39,7 +39,7 @@ public class ValidationContext { } } - public void addResult(final boolean test, final String message) { + public void addErrorIfTestFailed(final boolean test, final String message) { if (!test) { addResult(ValidationResult.error(new ValidationException(message))); } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationResult.java b/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationResult.java index 36cd9fd31a656701b2d68210db4eeb1561ba5caa..129780e15ceb875ba1a9538599e2ccd3716bd26a 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationResult.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/domain/validation/ValidationResult.java @@ -1,39 +1,135 @@ package dev.fitko.fitconnect.api.domain.validation; +import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; + +import java.util.ArrayList; +import java.util.List; + /** * Wrapper for validations including an exception */ public final class ValidationResult { private final boolean isValid; - private Exception error; + private final Exception error; + + private final List<Problem> validationProblems = new ArrayList<>(); private ValidationResult(final boolean isValid) { this.isValid = isValid; + error = null; } - private ValidationResult(final boolean isValid, final Exception exception) { + private ValidationResult(final boolean isValid, final Exception error) { this.isValid = isValid; - this.error = exception; + this.error = error; + } + + private ValidationResult(final Problem problem) { + isValid = false; + error = null; + validationProblems.add(problem); } + private ValidationResult(final List<Problem> problems) { + isValid = false; + error = null; + validationProblems.addAll(problems); + } + + private ValidationResult(final Exception error, final Problem problem) { + isValid = false; + this.error = error; + validationProblems.add(problem); + } + + /** + * Create new valid result. + * + * @return the valid result + */ public static ValidationResult ok() { return new ValidationResult(true); } + /** + * Create new failed result with an exception. + * + * @return the invalid result + */ public static ValidationResult error(final Exception exception) { return new ValidationResult(false, exception); } + /** + * Create new failed result with a {@link Problem}. + * + * @return the invalid result + */ + public static ValidationResult problem(final Problem problem) { + return new ValidationResult(problem); + } + + /** + * Create new failed result with a list of {@link Problem}. + * + * @return the invalid result + */ + public static ValidationResult problems(final List<Problem> problems) { + return new ValidationResult(problems); + } + + /** + * Create new failed result with an Exception and a {@link Problem}. + * + * @return the invalid result + */ + public static ValidationResult withErrorAndProblem(final Exception exception, final Problem problem) { + return new ValidationResult(exception, problem); + } + + /** + * Successful validation without errors. + * + * @return true if valid + */ public boolean isValid() { - return this.isValid; + return isValid; } + /** + * Failed validation with an error. + * + * @return true if an error occurred + */ public boolean hasError() { - return !this.isValid || this.error != null; + return error != null; + } + + /** + * Failed validation with a problem error that gets auto-rejected. + * + * @return true if a problem occurred + */ + public boolean hasProblems() { + return !validationProblems.isEmpty(); + } + + /** + * Gets the problem that was detected during validation. + * + * @return {@link Problem} + */ + public List<Problem> getProblems() { + return validationProblems; } + /** + * Gets the exception that occurred during validation. + * + * @return {@link Exception} + */ public Exception getError() { - return this.error; + return error; } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/AuthenticationTagsEmptyException.java b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/AuthenticationTagsEmptyException.java new file mode 100644 index 0000000000000000000000000000000000000000..8b4ad53aa037dc3cde571b77dde849582a952d83 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/AuthenticationTagsEmptyException.java @@ -0,0 +1,12 @@ +package dev.fitko.fitconnect.api.exceptions; + +public class AuthenticationTagsEmptyException extends RuntimeException { + + public AuthenticationTagsEmptyException(final String errorMessage, final Throwable error) { + super(errorMessage, error); + } + + public AuthenticationTagsEmptyException(final String errorMessage) { + super(errorMessage); + } +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java index 6fdfc7cd3567d182f658a81c1d9add446743ca1a..f745958c039fd7d0d1004aec734b5f178bd72fc3 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/RestApiException.java @@ -1,7 +1,13 @@ package dev.fitko.fitconnect.api.exceptions; +import lombok.Getter; +import org.springframework.http.HttpStatus; + public class RestApiException extends RuntimeException { + @Getter + private HttpStatus httpStatus; + public RestApiException(final String errorMessage, final Throwable error) { super(errorMessage, error); } @@ -9,4 +15,9 @@ public class RestApiException extends RuntimeException { public RestApiException(final String errorMessage) { super(errorMessage); } + + public RestApiException(final HttpStatus httpStatus, final String errorMessage) { + super(errorMessage); + this.httpStatus = httpStatus; + } } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/SubmissionRequestException.java b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/SubmissionRequestException.java new file mode 100644 index 0000000000000000000000000000000000000000..708c3355cbee2f243e09dd4f6771d37d8f48fe83 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/SubmissionRequestException.java @@ -0,0 +1,12 @@ +package dev.fitko.fitconnect.api.exceptions; + +public class SubmissionRequestException extends RuntimeException { + + public SubmissionRequestException(final String errorMessage, final Throwable error) { + super(errorMessage, error); + } + + public SubmissionRequestException(final String errorMessage) { + super(errorMessage); + } +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/exceptions/SubmitEventNotFoundException.java b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/SubmitEventNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..5075c4886298d75067ea9de71fcef7348c1a89c0 --- /dev/null +++ b/api/src/main/java/dev/fitko/fitconnect/api/exceptions/SubmitEventNotFoundException.java @@ -0,0 +1,12 @@ +package dev.fitko.fitconnect.api.exceptions; + +public class SubmitEventNotFoundException extends RuntimeException { + + public SubmitEventNotFoundException(final String errorMessage, final Throwable error) { + super(errorMessage, error); + } + + public SubmitEventNotFoundException(final String errorMessage) { + super(errorMessage); + } +} diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/Sender.java b/api/src/main/java/dev/fitko/fitconnect/api/services/Sender.java index 63ce5c0755475a1d74aa11c0446676dda3d8915b..d7ce15ff790f1de2d350bfa0132a7239ce5559e0 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/services/Sender.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/Sender.java @@ -28,21 +28,6 @@ import java.util.UUID; */ public interface Sender { - /** - * Validates the public key consisting of the following steps: - * <p> - * <ul> - * <li>checks if the JSON Web Key is suitable for the encryption</li> - * <li>checks if the public key is matching the certificate referenced in the JWK</li> - * <li>checks the certificate chain up to the root certificate</li> - * <li>checks against a certificate revocation list and/or an OSCP-endpoint with signed response</li> - * </ul> - * @param publicKey the public JWK - * - * @return {@link ValidationResult} that includes an error if the validation failed - */ - ValidationResult validatePublicKey(RSAKey publicKey); - /** * Validates the {@link Metadata} structure against a given JSON-schema to ensure its correctness. * diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/Subscriber.java b/api/src/main/java/dev/fitko/fitconnect/api/services/Subscriber.java index 3c612b212cc194c8a1536d7c4d6cda31fffd617e..946fed1a5daaaf43de0c92f98b37fb90c323e716 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/services/Subscriber.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/Subscriber.java @@ -2,14 +2,19 @@ package dev.fitko.fitconnect.api.services; import com.nimbusds.jose.jwk.RSAKey; import dev.fitko.fitconnect.api.domain.model.destination.Destination; +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.EventPayload; +import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; import dev.fitko.fitconnect.api.domain.model.metadata.attachment.ApiAttachment; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation; import dev.fitko.fitconnect.api.domain.model.metadata.data.Data; import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.DecryptionException; +import dev.fitko.fitconnect.api.exceptions.RestApiException; import java.util.List; import java.util.Set; @@ -29,7 +34,7 @@ public interface Subscriber { * @param encryptedContent JWE encrypted content that should be decrypted * @return the decrypted content as byte[] */ - byte[] decryptStringContent(RSAKey privateKey, String encryptedContent); + byte[] decryptStringContent(RSAKey privateKey, String encryptedContent) throws DecryptionException; /** * Polls available {@link SubmissionForPickup}s for a given destinationId. @@ -39,7 +44,7 @@ public interface Subscriber { * @param offset position in the dataset * @return list of found {@link SubmissionForPickup}s */ - Set<SubmissionForPickup> pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit); + Set<SubmissionForPickup> pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit) throws RestApiException; /** * Polls available {@link SubmissionForPickup}s for all destinations the {@link Subscriber} is assigned to. @@ -56,7 +61,7 @@ public interface Subscriber { * @param submissionId the unique identifier of a {@link Submission} * @return the requested {@link Submission} */ - Submission getSubmission(UUID submissionId); + Submission getSubmission(UUID submissionId) throws RestApiException; /** * Loads encrypted {@link ApiAttachment} for a {@link Submission}. @@ -65,7 +70,7 @@ public interface Subscriber { * @param attachmentId unique identifier of the attachments * @return encrypted JWE string of attachment */ - String fetchAttachment(UUID submissionId, UUID attachmentId); + String fetchAttachment(UUID submissionId, UUID attachmentId) throws RestApiException; /** * Retrieve the entire event log for a submissions caseId of a specific destination. @@ -74,15 +79,38 @@ public interface Subscriber { * @param destinationId unique identifier of the {@link Destination} the log should be retrieved for * @return List of {@link EventLogEntry}s for the given case */ - List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId); + List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId) throws RestApiException; /** - * Validates the {@link Metadata} structure against a given JSON-schema to ensure its correctness. + * Validates the {@link Metadata} structure and contents to ensure its correctness. * * @param metadata the {@link Metadata} object that is validated - * @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid or doesn't match the schema + * @param submission the {@link Submission} of the validated metadata + * @param authenticationTags the {@link AuthenticationTags} of the validated metadata + * @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema */ - ValidationResult validateMetadata(Metadata metadata); + ValidationResult validateMetadata(Metadata metadata, Submission submission, AuthenticationTags authenticationTags); + + /** + * Validates the attachment structure and contents to ensure its correctness. + * + * @param attachmentsForValidation list of attachments containing the hash, decrypted and encrypted data needed for validation + * @param authenticationTags the {@link AuthenticationTags} of the validated attachments + + * @return a {@link ValidationResult}, contains an error if the attachment is invalid + */ + ValidationResult validateAttachments(List<AttachmentForValidation> attachmentsForValidation, AuthenticationTags authenticationTags); + + /** + * Validates the {@link Data} structure and contents to ensure its correctness. + * + * @param data the unencrypted data as byte[] + * @param submission the {@link Submission} of the validated data + * @param metadata the {@link Metadata} of the validated data + * @param authenticationTags the {@link AuthenticationTags} of the validated data + * @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema + */ + ValidationResult validateData(byte[] data, Submission submission, Metadata metadata, AuthenticationTags authenticationTags); /** * Validates data integrity of {@link ApiAttachment} or {@link Data}. @@ -119,4 +147,13 @@ public interface Subscriber { * @see <a href="https://docs.fitko.de/fit-connect/docs/receiving/process-and-acknowledge">Process And Acknowledge</a> */ void rejectSubmission(EventPayload eventPayload); + + /** + * Get event from event-log filtered by a given event and submission. + * + * @param event event type to filter the event-log by + * @param submission submission to filter the event-log by + * @return {@link AuthenticationTags} of metadata, data and attachments + */ + AuthenticationTags getAuthenticationTagsForEvent(Event event, Submission submission); } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/events/EventLogService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/events/EventLogService.java index bef075653ed6bca6d95aa9a09a56438696f00aa1..095dfa3982a1c4d1b46a1a10573f3b4afc8a8e57 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/services/events/EventLogService.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/events/EventLogService.java @@ -1,13 +1,13 @@ package dev.fitko.fitconnect.api.services.events; import dev.fitko.fitconnect.api.domain.model.destination.Destination; +import dev.fitko.fitconnect.api.domain.model.event.Event; import dev.fitko.fitconnect.api.domain.model.event.EventLog; import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry; import dev.fitko.fitconnect.api.domain.model.event.SubmissionStatus; import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.exceptions.EventLogException; -import dev.fitko.fitconnect.api.exceptions.RestApiException; import java.util.List; import java.util.UUID; @@ -27,8 +27,9 @@ public interface EventLogService { * @param caseId unique case identifier * @param destinationId unique identifier of the destination * @return list of {@link EventLogEntry} + * @throws EventLogException if a technical error occurred or the validation failed */ - List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId) throws RestApiException, EventLogException; + List<EventLogEntry> getEventLog(UUID caseId, UUID destinationId) throws EventLogException; /** * Retrieve the current status of a {@link Submission}. @@ -37,16 +38,27 @@ public interface EventLogService { * @param caseId unique identifier of the case the log should be retrieved for * @param submissionId unique identifier of the submission the log should be retrieved for * @param authenticationTags {@link AuthenticationTags} used for SET-Event integrity validation - * * @return {@link SubmissionStatus} the current status + * @throws EventLogException if a technical error occurred or the validation failed */ - SubmissionStatus getLastedEvent(UUID destinationId, UUID caseId, UUID submissionId, AuthenticationTags authenticationTags) throws RestApiException, EventLogException; + SubmissionStatus getLastedEvent(UUID destinationId, UUID caseId, UUID submissionId, AuthenticationTags authenticationTags) throws EventLogException; /** * Send an event for a given caseId. * * @param caseId unique case identifier * @param signedAndSerializedSET the serialised and signed event as SET string + * @throws EventLogException if a technical error occurred + */ + void sendEvent(UUID caseId, String signedAndSerializedSET) throws EventLogException; + + /** + * Get authentication tags for a given event and caseId. + * + * @param event the event type to filter the log for + * @param submission submission data + * @return {@link AuthenticationTags} for metadata, data and attachments + * @throws EventLogException if a technical error occurred or the validation failed */ - void sendEvent(UUID caseId, String signedAndSerializedSET) throws RestApiException; + AuthenticationTags getAuthenticationTagsForEvent(Event event, Submission submission) throws EventLogException; } 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 040dc4166d8c5b001ff46a63f5ac566f432e11c6..e627b3cefa2fa1fe9e3131672e017ea7d653c3af 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 @@ -1,5 +1,6 @@ package dev.fitko.fitconnect.api.services.keys; +import com.nimbusds.jose.jwk.KeyOperation; import com.nimbusds.jose.jwk.RSAKey; import dev.fitko.fitconnect.api.domain.model.destination.Destination; import dev.fitko.fitconnect.api.services.validation.ValidationService; @@ -19,7 +20,7 @@ public interface KeyService { * * @param destinationId unique identifier of the {@link Destination} * - * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)}) + * @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}}) */ RSAKey getPublicEncryptionKey(UUID destinationId); @@ -28,7 +29,7 @@ public interface KeyService { * * @param destinationId unique identifier of the {@link Destination} * @param keyId unique identifier of the {@link RSAKey} - * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)}) + * @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}}) */ RSAKey getPublicSignatureKey(UUID destinationId, String keyId); @@ -36,7 +37,7 @@ public interface KeyService { * Get a public signature key for a given key-id from the self-service portal well-known keys. * * @param keyId unique identifier of the {@link RSAKey} - * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)}) + * @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}}) */ RSAKey getPortalPublicKey(String keyId); @@ -44,7 +45,7 @@ public interface KeyService { * Get a public signature key for a given key-id from the submission service well-known keys. * * @param keyId unique identifier of the {@link RSAKey} - * @return validated {@link RSAKey} (@see {@link ValidationService#validateEncryptionPublicKey(RSAKey)}) + * @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}}) */ RSAKey getSubmissionServicePublicKey(String keyId); @@ -54,7 +55,7 @@ public interface KeyService { * * @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)}) + * @return validated {@link RSAKey} (@see {@link ValidationService#validatePublicKey(RSAKey, KeyOperation)}}) */ RSAKey getWellKnownKeysForSubmissionUrl(String url, String keyId); } diff --git a/api/src/main/java/dev/fitko/fitconnect/api/services/submission/SubmissionService.java b/api/src/main/java/dev/fitko/fitconnect/api/services/submission/SubmissionService.java index 2256452c8c5e9520519e8dd4bccd4d473628f675..5f49f41da3c6b6bff5c30ef75f3bf1f5e3c1308e 100644 --- a/api/src/main/java/dev/fitko/fitconnect/api/services/submission/SubmissionService.java +++ b/api/src/main/java/dev/fitko/fitconnect/api/services/submission/SubmissionService.java @@ -30,7 +30,7 @@ public interface SubmissionService { * * @see <a href="https://docs.fitko.de/fit-connect/docs/sending/start-submission">Announcing a submission</a> */ - SubmissionForPickup announceSubmission(CreateSubmission submission); + SubmissionForPickup announceSubmission(CreateSubmission submission) throws RestApiException; /** * Send a submission that was already {@link #announceSubmission(CreateSubmission) announced} before. @@ -38,7 +38,7 @@ public interface SubmissionService { * @param submission submission including the encrypted {@link Data} and {@link Metadata} * @return the submission that was sent */ - Submission sendSubmission(SubmitSubmission submission); + Submission sendSubmission(SubmitSubmission submission) throws RestApiException; /** * Get a {@link Submission} by id. @@ -46,7 +46,7 @@ public interface SubmissionService { * @param submissionId unique submission identifier * @return submission matching the given id */ - Submission getSubmission(UUID submissionId); + Submission getSubmission(UUID submissionId) throws RestApiException; /** * Get all available submissions. @@ -65,7 +65,7 @@ public interface SubmissionService { * @param limit number of submissions in result (max. is 500) * @return SubmissionsForPickup containing a list of submissions */ - SubmissionsForPickup pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit); + SubmissionsForPickup pollAvailableSubmissionsForDestination(UUID destinationId, int offset, int limit) throws RestApiException; /** * Upload an encrypted {@link ApiAttachment}. @@ -74,7 +74,7 @@ public interface SubmissionService { * @param attachmentId unique destination identifier * @param encryptedAttachment JWE encrypted attachment payload */ - void uploadAttachment(UUID submissionId, UUID attachmentId, String encryptedAttachment); + void uploadAttachment(UUID submissionId, UUID attachmentId, String encryptedAttachment) throws RestApiException; /** * Get an {@link ApiAttachment} by id for a given {@link Submission}. @@ -83,7 +83,7 @@ public interface SubmissionService { * @param attachmentId unique attachment identifier * @return encrypted string of the attachment data */ - String getAttachment(UUID submissionId, UUID attachmentId); + String getAttachment(UUID submissionId, UUID attachmentId) throws RestApiException; /** * Get the submissions {@link Destination} by id. @@ -93,5 +93,5 @@ public interface SubmissionService { * * @throws RestApiException if an error occurred */ - Destination getDestination(UUID destinationID); + Destination getDestination(UUID destinationID) throws RestApiException; } 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 c5d747b3f9e54e225c6f9ef0d55aad86628c4f4d..8941384a1f953f6fdd0d2cf2c1b8fe0f356278e8 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 @@ -1,9 +1,16 @@ package dev.fitko.fitconnect.api.services.validation; +import com.nimbusds.jose.jwk.KeyOperation; import com.nimbusds.jose.jwk.RSAKey; +import dev.fitko.fitconnect.api.domain.model.destination.Destination; +import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation; +import dev.fitko.fitconnect.api.domain.model.metadata.data.Data; +import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import java.util.List; import java.util.Map; /** @@ -14,28 +21,14 @@ import java.util.Map; public interface ValidationService { /** - * Validates the public key consisting of the following steps: - * <p> - * <ul> - * <li>checks if the JSON Web Key has key operation `wrap_key`</li> - * <li>checks if the JSON Web Key is suitable for the encryption</li> - * <li>checks if the public key is matching the certificate referenced in the JWK</li> - * <li>checks the certificate chain up to the root certificate</li> - * <li>checks against a certificate revocation list and/or an OSCP-endpoint with signed response</li> - * </ul> - * @param publicKey the public JWK + * Validates the public key for integrity. * - * @return {@link ValidationResult} that includes an error if the validation failed - */ - ValidationResult validateEncryptionPublicKey(RSAKey publicKey); - - /** - * Validates the public signature key with key-operation `verify` + * @param publicKey the public JWK + * @param keyOperation key operation the public key be validated with, represents {@code key_ops} parameter in a JWK * - * @param signatureKey the public signature JWK * @return {@link ValidationResult} that includes an error if the validation failed */ - ValidationResult validateSignaturePublicKey(RSAKey signatureKey); + ValidationResult validatePublicKey(final RSAKey publicKey, KeyOperation keyOperation); /** * Validates the metadata against a given schema. @@ -46,6 +39,38 @@ public interface ValidationService { */ ValidationResult validateMetadataSchema(Metadata metadata); + /** + * Validates the {@link Metadata} structure and contents to ensure its correctness. + * + * @param metadata the {@link Metadata} object that is validated + * @param submission the {@link Submission} of the validated metadata + * @param destination the {@link Destination} of the validated metadata + * @param authenticationTags the {@link AuthenticationTags} of the validated metadata + * @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema + */ + ValidationResult validateMetadata(Metadata metadata, Submission submission, Destination destination, AuthenticationTags authenticationTags); + + /** + * Validates the attachment structure and contents to ensure its correctness. + * + * @param attachmentsForValidation list of attachments containing the hash, decrypted and encrypted data needed for validation + * @param authenticationTags the {@link AuthenticationTags} of the validated attachments + + * @return a {@link ValidationResult}, contains an error if the attachment is invalid + */ + ValidationResult validateAttachments(List<AttachmentForValidation> attachmentsForValidation, final AuthenticationTags authenticationTags); + + /** + * Validates the {@link Data} structure and contents to ensure its correctness. + * + * @param decryptedData the unencrypted data as byte[] + * @param submission the {@link Submission} of the validated data + * @param metadata the {@link Metadata} of the validated data + * @param authenticationTags the {@link AuthenticationTags} of the validated data + * @return a {@link ValidationResult}, contains an error if the {@link Metadata} is invalid, e.g. doesn't match the schema + */ + ValidationResult validateData(byte[] decryptedData, Submission submission, Metadata metadata, AuthenticationTags authenticationTags); + /** * Validates a set event against a given schema. * @@ -101,4 +126,5 @@ public interface ValidationService { * @return {@code true} if hmac and timestamp provided by the callback meet the required conditions */ ValidationResult validateCallback(String hmac, Long timestamp, String httpBody, String callbackSecret); + } diff --git a/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java b/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java index f91e6e2956ab541239f57752b93247855fcb27f1..c4438300daff840e8dcc9090eb3688004a22651b 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/RoutingClient.java @@ -30,7 +30,6 @@ public final class RoutingClient { * @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()); 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 f33c5c9045b288413700e808b615d3d4720b1db2..dadba3618a93729b84f820d870f0b06ca353e384 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/SubscriberClient.java @@ -1,7 +1,5 @@ package dev.fitko.fitconnect.client; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.jwk.RSAKey; import dev.fitko.fitconnect.api.domain.model.destination.Destination; import dev.fitko.fitconnect.api.domain.model.event.Event; import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry; @@ -15,23 +13,17 @@ import dev.fitko.fitconnect.api.domain.model.submission.SentSubmission; import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; -import dev.fitko.fitconnect.api.exceptions.DataIntegrityException; -import dev.fitko.fitconnect.api.exceptions.DecryptionException; -import dev.fitko.fitconnect.api.exceptions.EventCreationException; -import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.exceptions.SubmissionRequestException; import dev.fitko.fitconnect.api.services.Subscriber; import dev.fitko.fitconnect.client.sender.model.Attachment; import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission; import dev.fitko.fitconnect.client.subscriber.model.DecryptedAttachmentPayload; import dev.fitko.fitconnect.client.subscriber.model.ReceivedData; import dev.fitko.fitconnect.core.util.StopWatch; +import dev.fitko.fitconnect.client.subscriber.SubmissionReceiver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -46,14 +38,15 @@ import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKN public class SubscriberClient { private static final Logger LOGGER = LoggerFactory.getLogger(SubscriberClient.class); - private static final ObjectMapper MAPPER = new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final int DEFAULT_SUBMISSION_LIMIT = 500; + private final Subscriber subscriber; - private final RSAKey privateKey; + private final SubmissionReceiver submissionReceiver; - public SubscriberClient(final Subscriber subscriber, final RSAKey privateKey) { + public SubscriberClient(final Subscriber subscriber, final SubmissionReceiver submissionReceiver) { this.subscriber = subscriber; - this.privateKey = privateKey; + this.submissionReceiver = submissionReceiver; } /** @@ -103,59 +96,14 @@ public class SubscriberClient { } /** - * Loads a single {@link SubmissionForPickup} by id. + * Loads a single {@link SubmissionForPickup} by id. Auto-rejects invalid submissions where e.g. a validation failed. * - * @param submissionId unique identifier of a submission - * @return {@link ReceivedSubmission} to get the submission {@link Metadata}, {@link Data} and {@link ApiAttachment}s as well as - * accept or reject the loaded submission + * @param submissionId unique identifier of the requested submission + * @return {@link ReceivedSubmission} to get the submission {@link Metadata}, {@link Data} and {@link Attachment}s as well as accept or reject the loaded submission + * @throws SubmissionRequestException if a technical error occurred or validation failed */ - public ReceivedSubmission requestSubmission(final UUID submissionId) { - - try { - - final var startTimeDownloadSubmission = StopWatch.start(); - final Submission submission = subscriber.getSubmission(submissionId); - LOGGER.info("Downloading submission took {}", StopWatch.stopWithFormattedTime(startTimeDownloadSubmission)); - - LOGGER.info("Decrypting metadata ..."); - final Metadata metadata = decryptMetadata(submission.getEncryptedMetadata()); - final ValidationResult metadataValidation = subscriber.validateMetadata(metadata); - if (metadataValidation.hasError()) { - LOGGER.error("Metadata does not match schema", metadataValidation.getError()); - return null; - } - - LOGGER.info("Decrypting data ..."); - final byte[] decryptedData = decryptData(submission.getEncryptedData()); - final String hashFromSender = getDataHashFromMetadata(metadata); - final ValidationResult dataValidation = subscriber.validateHashIntegrity(hashFromSender, decryptedData); - if (dataValidation.hasError()) { - LOGGER.error("Data might be corrupted, hash-sum does not match", dataValidation.getError()); - return null; - } - - LOGGER.info("Loading and decrypting attachments ..."); - final List<ApiAttachment> attachmentMetadata = metadata.getContentStructure().getAttachments(); - final List<DecryptedAttachmentPayload> decryptedAttachmentPayloads = loadAttachments(submissionId, attachmentMetadata); - final ValidationResult attachmentValidation = validateAttachments(decryptedAttachmentPayloads); - if (attachmentValidation.hasError()) { - LOGGER.error("One of the attachments is invalid", attachmentValidation.getError()); - return null; - } - - return buildReceivedSubmission(submission, metadata, decryptedData, decryptedAttachmentPayloads); - - } catch (final DecryptionException e) { - LOGGER.error("Decrypting payload failed", e); - } catch (final RestApiException e) { - LOGGER.error("API request failed", e); - } catch (final EventCreationException e) { - LOGGER.error("Creating SET event failed", e); - } catch (final IOException e) { - LOGGER.error("Reading metadata failed", e); - } - - return null; + public ReceivedSubmission requestSubmission(final UUID submissionId) throws SubmissionRequestException { + return submissionReceiver.receiveSubmission(submissionId); } /** @@ -169,83 +117,16 @@ public class SubscriberClient { subscriber.rejectSubmission(EventPayload.forRejectEvent(submissionForPickup, rejectionProblems)); } + /** + * Checks if a received callback can be trusted by validating the provided request data + * + * @param hmac authentication code provided by the callback + * @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 + */ 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<Attachment> receivedAttachments = attachments.stream().map(this::mapToAttachment).collect(Collectors.toList()); - // TODO setting auth tags was refactored in #598-auto-reject - return new ReceivedSubmission(subscriber, submission, metadata, receivedData, receivedAttachments, Map.of()); - } - - private Attachment mapToAttachment(final DecryptedAttachmentPayload payload) { - final ApiAttachment metadata = payload.getAttachmentMetadata(); - return Attachment.fromByteArray(payload.getDecryptedContent(), metadata.getMimeType(), metadata.getFilename(), metadata.getDescription()); - } - - private List<DecryptedAttachmentPayload> loadAttachments(final UUID submissionId, final List<ApiAttachment> attachmentMetadata) { - if (attachmentMetadata == null || attachmentMetadata.isEmpty()) { - LOGGER.info("Submission contains no attachments"); - return Collections.emptyList(); - } - final List<DecryptedAttachmentPayload> receivedAttachments = new ArrayList<>(); - for (final ApiAttachment metadata : attachmentMetadata) { - final String encryptedAttachment = downloadAttachment(submissionId, metadata); - final byte[] decryptedAttachment = decryptAttachment(metadata, encryptedAttachment); - final DecryptedAttachmentPayload decryptedAttachmentPayload = DecryptedAttachmentPayload.builder() - .decryptedContent(decryptedAttachment) - .attachmentMetadata(metadata) - .build(); - receivedAttachments.add(decryptedAttachmentPayload); - } - return receivedAttachments; - } - - private byte[] decryptAttachment(final ApiAttachment metadata, final String encryptedAttachment) { - final var startDecryption = StopWatch.start(); - final byte[] decryptedAttachment = subscriber.decryptStringContent(privateKey, encryptedAttachment); - LOGGER.info("Decrypting attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDecryption)); - return decryptedAttachment; - } - - private String downloadAttachment(final UUID submissionId, final ApiAttachment metadata) { - final var startDownload = StopWatch.start(); - final String encryptedAttachment = subscriber.fetchAttachment(submissionId, metadata.getAttachmentId()); - LOGGER.info("Downloading attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDownload)); - return encryptedAttachment; - } - - private byte[] decryptData(final String encryptedData) { - return subscriber.decryptStringContent(privateKey, encryptedData); - } - - private Metadata decryptMetadata(final String encryptedMetadata) throws IOException { - final byte[] metadataBytes = subscriber.decryptStringContent(privateKey, encryptedMetadata); - return MAPPER.readValue(metadataBytes, Metadata.class); - } - - private ValidationResult validateAttachments(final List<DecryptedAttachmentPayload> decryptedAttachmentPayloads) { - for (final DecryptedAttachmentPayload decryptedAttachment : decryptedAttachmentPayloads) { - final ApiAttachment attachmentMetadata = decryptedAttachment.getAttachmentMetadata(); - final UUID attachmentId = attachmentMetadata.getAttachmentId(); - final ValidationResult result = subscriber.validateHashIntegrity(attachmentMetadata.getHash().getContent(), decryptedAttachment.getDecryptedContent()); - if (result.hasError()) { - LOGGER.error("Attachment data for id {} is corrupted", attachmentId, result.getError()); - return ValidationResult.error(new DataIntegrityException("Attachment " + attachmentId + " is corrupt")); - } else { - LOGGER.info("Attachment {} with id {} is valid", attachmentMetadata.getFilename(), attachmentId); - } - } - 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/ClientFactory.java b/client/src/main/java/dev/fitko/fitconnect/client/factory/ClientFactory.java index 3c5e148f6b30ef550c60135231c9d800d2f14e6d..55af02f9afbe5daa0a203463f756157c3f7fe896 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 @@ -25,6 +25,7 @@ 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.client.subscriber.SubmissionReceiver; import dev.fitko.fitconnect.core.SubmissionSender; import dev.fitko.fitconnect.core.SubmissionSubscriber; import dev.fitko.fitconnect.core.auth.DefaultOAuthService; @@ -98,7 +99,9 @@ public final class ClientFactory { final String privateKeyPath = readPath(getPrivateDecryptionKeyPathFromSubscriber(subscriberConfig), "Decryption Key"); final RSAKey privateKey = readRSAKeyFromString(privateKeyPath); - return new SubscriberClient(subscriber, privateKey); + final SubmissionReceiver submissionReceiver = new SubmissionReceiver(subscriber, privateKey, config); + + return new SubscriberClient(subscriber, submissionReceiver); } /** * Create a new {@link RoutingClient} to find destinations and services that is automatically configured via a provided {@link ApplicationConfig}. diff --git a/client/src/main/java/dev/fitko/fitconnect/client/sender/model/Attachment.java b/client/src/main/java/dev/fitko/fitconnect/client/sender/model/Attachment.java index adae701b27f894181954fa0cc3a022018d1b80a3..932fe4ae7c3dc4e673997642dc045eeca63cbe91 100644 --- a/client/src/main/java/dev/fitko/fitconnect/client/sender/model/Attachment.java +++ b/client/src/main/java/dev/fitko/fitconnect/client/sender/model/Attachment.java @@ -129,6 +129,15 @@ public class Attachment { return new String(data, encoding); } + /** + * Get the attachment content as string with a default UTF-8 encoding, e.g. in case the mime-type is json or xml. + * + * @return utf-8 encoded string of the attachments content. + */ + public String getDataAString() { + return new String(data, StandardCharsets.UTF_8); + } + /** * Filename of the attachment. This filed is optional so it might be null. * @return filename as string, null if not present diff --git a/client/src/main/java/dev/fitko/fitconnect/client/subscriber/SubmissionReceiver.java b/client/src/main/java/dev/fitko/fitconnect/client/subscriber/SubmissionReceiver.java new file mode 100644 index 0000000000000000000000000000000000000000..050f5c8d1186ac1759759f789fa2eb6570d2135e --- /dev/null +++ b/client/src/main/java/dev/fitko/fitconnect/client/subscriber/SubmissionReceiver.java @@ -0,0 +1,252 @@ +package dev.fitko.fitconnect.client.subscriber; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.jwk.KeyUse; +import com.nimbusds.jose.jwk.RSAKey; +import dev.fitko.fitconnect.api.config.ApplicationConfig; +import dev.fitko.fitconnect.api.domain.model.event.Event; +import dev.fitko.fitconnect.api.domain.model.event.EventPayload; +import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; +import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.AttachmentEncryptionIssue; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.MissingAttachment; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataEncryptionIssue; +import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.MetadataEncryptionIssue; +import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.MetadataJsonSyntaxViolation; +import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog; +import dev.fitko.fitconnect.api.domain.model.event.problems.submission.MissingAuthenticationTags; +import dev.fitko.fitconnect.api.domain.model.event.problems.submission.NotExactlyOneSubmitEvent; +import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.ApiAttachment; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation; +import dev.fitko.fitconnect.api.domain.model.metadata.data.MimeType; +import dev.fitko.fitconnect.api.domain.model.submission.Submission; +import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.AuthenticationTagsEmptyException; +import dev.fitko.fitconnect.api.exceptions.DecryptionException; +import dev.fitko.fitconnect.api.exceptions.EventLogException; +import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.exceptions.SubmissionRequestException; +import dev.fitko.fitconnect.api.exceptions.SubmitEventNotFoundException; +import dev.fitko.fitconnect.api.services.Subscriber; +import dev.fitko.fitconnect.client.sender.model.Attachment; +import dev.fitko.fitconnect.client.subscriber.model.ReceivedData; +import dev.fitko.fitconnect.core.util.StopWatch; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static com.nimbusds.jose.jwk.KeyOperation.UNWRAP_KEY; + +public class SubmissionReceiver { + + private static final Logger LOGGER = LoggerFactory.getLogger(SubmissionReceiver.class); + private static final ObjectMapper MAPPER = new ObjectMapper().configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + + private final Subscriber subscriber; + private final ApplicationConfig config; + private final RSAKey privateKey; + + public SubmissionReceiver(final Subscriber subscriber, final RSAKey privateKey, final ApplicationConfig config) { + this.subscriber = subscriber; + this.privateKey = privateKey; + this.config = config; + } + + public ReceivedSubmission receiveSubmission(final UUID submissionId) { + + LOGGER.info("Requesting submission ..."); + final Submission submission = loadSubmission(submissionId); + + LOGGER.info("Loading authentication tags from event log ..."); + final AuthenticationTags authenticationTags = loadAuthTagsForSubmitEvent(submission); + + LOGGER.info("Decrypting metadata ..."); + final Metadata metadata = decryptMetadata(submission); + validateMetadata(metadata, submission, authenticationTags); + + LOGGER.info("Decrypting data ..."); + final byte[] decryptedData = decryptData(submission); + validateData(submission, metadata, decryptedData, authenticationTags); + + LOGGER.info("Loading and decrypting attachments ..."); + final List<AttachmentForValidation> attachments = loadAttachments(submission, metadata); + validateAttachments(attachments, submission, authenticationTags); + + return buildReceivedSubmission(submission, metadata, decryptedData, attachments); + } + + private Submission loadSubmission(final UUID submissionId) { + try { + final var startTimeDownloadSubmission = StopWatch.start(); + final Submission submission = subscriber.getSubmission(submissionId); + LOGGER.info("Downloading submission took {}", StopWatch.stopWithFormattedTime(startTimeDownloadSubmission)); + return submission; + } catch (final RestApiException e) { + throw new SubmissionRequestException(e.getMessage(), e); + } + } + + private AuthenticationTags loadAuthTagsForSubmitEvent(final Submission submission) { + try { + return subscriber.getAuthenticationTagsForEvent(Event.SUBMIT, submission); + } catch (final EventLogException e) { + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#struktur--und-signaturpr%C3%BCfung-der-security-event-tokens + final InvalidEventLog problem = new InvalidEventLog(); + rejectSubmissionWithProblem(submission, problem); + throw new SubmissionRequestException(problem.getDetail(), e); + } catch (final SubmitEventNotFoundException e) { + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#genau-ein-submit-event + final NotExactlyOneSubmitEvent problem = new NotExactlyOneSubmitEvent(); + rejectSubmissionWithProblem(submission, problem); + throw new SubmissionRequestException(problem.getDetail()); + } catch (final AuthenticationTagsEmptyException e) { + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#authentication-tags-im-submit-event + final MissingAuthenticationTags problem = new MissingAuthenticationTags(); + rejectSubmissionWithProblem(submission, problem); + throw new SubmissionRequestException(problem.getDetail()); + } + } + + private void validateMetadata(final Metadata metadata, final Submission submission, final AuthenticationTags authenticationTags) { + final ValidationResult validationResult = subscriber.validateMetadata(metadata, submission, authenticationTags); + evaluateValidationResult(submission, validationResult, "Metadata is invalid"); + } + + private void validateAttachments(final List<AttachmentForValidation> attachmentForValidation, final Submission submission, final AuthenticationTags authenticationTags) { + final ValidationResult validationResult = subscriber.validateAttachments(attachmentForValidation, authenticationTags); + evaluateValidationResult(submission, validationResult, "Attachment validation failed"); + } + + private void validateData(final Submission submission, final Metadata metadata, final byte[] decryptedData, final AuthenticationTags authenticationTags) { + final ValidationResult validationResult = subscriber.validateData(decryptedData, submission, metadata, authenticationTags); + evaluateValidationResult(submission, validationResult, "Data is invalid"); + } + + private void evaluateValidationResult(final Submission submission, final ValidationResult validationResult, final String errorMessage) throws SubmissionRequestException { + if (validationResult.hasProblems()) { + rejectSubmissionWithProblem(submission, validationResult.getProblems().toArray(new Problem[0])); + throw new SubmissionRequestException(validationResult.hasError() ? validationResult.getError().getMessage() : errorMessage); + } else if (validationResult.hasError()) { + LOGGER.error(validationResult.getError().getMessage(), validationResult.getError()); + throw new SubmissionRequestException(validationResult.getError().getMessage(), validationResult.getError()); + } + } + + private ReceivedSubmission buildReceivedSubmission(final Submission submission, final Metadata metadata, final byte[] decryptedData, final List<AttachmentForValidation> attachments) { + final MimeType mimeType = metadata.getContentStructure().getData().getSubmissionSchema().getMimeType(); + final ReceivedData receivedData = new ReceivedData(new String(decryptedData, StandardCharsets.UTF_8), mimeType); + final List<Attachment> receivedAttachments = attachments.stream().map(this::toAttachment).collect(Collectors.toList()); + final Map<UUID, String> encryptedAttachments = attachments.stream().collect(Collectors.toMap(AttachmentForValidation::getAttachmentId, AttachmentForValidation::getEncryptedData)); + return new ReceivedSubmission(subscriber, submission, metadata, receivedData, receivedAttachments, encryptedAttachments); + } + + private Attachment toAttachment(final AttachmentForValidation attachment) { + final ApiAttachment metadata = attachment.getAttachmentMetadata(); + return Attachment.fromByteArray(attachment.getDecryptedData(), metadata.getMimeType(), metadata.getFilename(), metadata.getDescription()); + } + + private List<AttachmentForValidation> loadAttachments(final Submission submission, final Metadata metadata) { + final List<ApiAttachment> attachments = metadata.getContentStructure().getAttachments(); + if (attachments == null || attachments.isEmpty()) { + LOGGER.info("Submission contains no attachments"); + return Collections.emptyList(); + } + final List<AttachmentForValidation> receivedAttachments = new ArrayList<>(); + for (final ApiAttachment attachmentMetadata : attachments) { + final String encryptedAttachment = downloadAttachment(submission, attachmentMetadata); + final byte[] decryptedAttachment = decryptAttachment(attachmentMetadata, encryptedAttachment, submission); + receivedAttachments.add(new AttachmentForValidation(attachmentMetadata, encryptedAttachment, decryptedAttachment)); + } + return receivedAttachments; + } + + private byte[] decryptAttachment(final ApiAttachment metadata, final String encryptedAttachment, final Submission submission) { + checkPrivateKey(); + try { + final var startDecryption = StopWatch.start(); + final byte[] decryptedAttachment = subscriber.decryptStringContent(privateKey, encryptedAttachment); + LOGGER.info("Decrypting attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDecryption)); + return decryptedAttachment; + } catch (final DecryptionException e) { + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung-2 + rejectSubmissionWithProblem(submission, new AttachmentEncryptionIssue(metadata.getAttachmentId())); + throw new SubmissionRequestException(e.getMessage(), e); + } + } + + private String downloadAttachment(final Submission submission, final ApiAttachment metadata) { + try { + final var startDownload = StopWatch.start(); + final String encryptedAttachment = subscriber.fetchAttachment(submission.getSubmissionId(), metadata.getAttachmentId()); + LOGGER.info("Downloading attachment {} took {}", metadata.getAttachmentId(), StopWatch.stopWithFormattedTime(startDownload)); + return encryptedAttachment; + } catch (final RestApiException e) { + if (e.getHttpStatus() != null && e.getHttpStatus().equals(HttpStatus.NOT_FOUND)) { + rejectSubmissionWithProblem(submission, new MissingAttachment(metadata.getAttachmentId())); + } + throw new SubmissionRequestException(e.getMessage(), e); + } + } + + private byte[] decryptData(final Submission submission) { + checkPrivateKey(); + try { + return subscriber.decryptStringContent(privateKey, submission.getEncryptedData()); + } catch (final DecryptionException e) { + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung-1 + rejectSubmissionWithProblem(submission, new DataEncryptionIssue()); + throw new SubmissionRequestException(e.getMessage(), e); + } + } + + private Metadata decryptMetadata(final Submission submission) { + checkPrivateKey(); + try { + final byte[] metadataBytes = subscriber.decryptStringContent(privateKey, submission.getEncryptedMetadata()); + return MAPPER.readValue(metadataBytes, Metadata.class); + } catch (final IOException e) { + rejectSubmissionWithProblem(submission, new MetadataJsonSyntaxViolation()); + throw new SubmissionRequestException(e.getMessage(), e); + } catch (final DecryptionException e) { + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#entschl%C3%BCsselung + rejectSubmissionWithProblem(submission, new MetadataEncryptionIssue()); + throw new SubmissionRequestException(e.getMessage(), e); + } + } + + private void checkPrivateKey() { + if (!isUnwrapKey(privateKey) && !isEncryptionKeyUse(privateKey)) { + throw new SubmissionRequestException("Private key is not suitable for decryption, could not find key_operation unwrapKey or key_use encryption"); + } + } + + private static boolean isEncryptionKeyUse(final RSAKey privateKey) { + return privateKey.getKeyUse() != null && privateKey.getKeyUse().equals(KeyUse.ENCRYPTION); + } + + private static boolean isUnwrapKey(final RSAKey privateKey) { + return privateKey.getKeyOperations() != null && privateKey.getKeyOperations().stream() + .anyMatch(keyOp -> keyOp.identifier().equals(UNWRAP_KEY.identifier())); + } + + private void rejectSubmissionWithProblem(final Submission submission, final Problem... problem) { + reject(submission, List.of(problem)); + } + + private void reject(final Submission submission, final List<Problem> problems) { + if (config.isEnableAutoReject()) { + subscriber.rejectSubmission(EventPayload.forRejectEvent(submission, problems)); + } + } +} diff --git a/client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java b/client/src/main/java/dev/fitko/fitconnect/client/subscriber/model/ReceivedAttachment.java similarity index 100% rename from client/src/test/java/dev/fitko/fitconnect/client/ClientIntegrationTest.java rename to client/src/main/java/dev/fitko/fitconnect/client/subscriber/model/ReceivedAttachment.java 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 118fbc8d6607723cd411002cbbf3a3af23ac87fe..bd084d15eea87238488c2a222a495efb700bbc45 100644 --- a/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java +++ b/client/src/test/java/dev/fitko/fitconnect/client/SenderClientTest.java @@ -102,7 +102,6 @@ public class SenderClientTest { when(senderMock.getDestination(any())).thenReturn(destination); when(senderMock.createSubmission(any())).thenReturn(announcedSubmission); when(senderMock.sendSubmission(any())).thenReturn(expectedSubmission); - when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok()); when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey); when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); @@ -144,7 +143,6 @@ public class SenderClientTest { when(senderMock.getDestination(any())).thenReturn(destination); when(senderMock.createSubmission(any())).thenReturn(announcedSubmission); when(senderMock.sendSubmission(any())).thenReturn(expectedSubmission); - when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok()); when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey); when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); when(senderMock.validateXmlFormat(any())).thenReturn(ValidationResult.ok()); @@ -184,7 +182,6 @@ public class SenderClientTest { when(senderMock.getDestination(any())).thenReturn(destination); when(senderMock.createSubmission(any())).thenReturn(announcedSubmission); - when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok()); when(senderMock.validateJsonFormat(any())).thenReturn(ValidationResult.ok()); when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey); @@ -379,7 +376,6 @@ public class SenderClientTest { final var destination = getDestination(destinationId); when(senderMock.getDestination(any())).thenReturn(destination); - when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok()); when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey); // When @@ -509,7 +505,6 @@ public class SenderClientTest { when(senderMock.getDestination(any())).thenReturn(destination); when(senderMock.createSubmission(any())).thenReturn(announcedSubmission); - when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok()); when(senderMock.getEncryptionKeyForDestination(any())).thenReturn(publicKey); when(senderMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); when(senderMock.validateXmlFormat(any())).thenReturn(ValidationResult.ok()); diff --git a/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java b/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java index 5aceb12ff04ee62b80a7cbb5bba57196ae4a649e..00e535a6f06020080d7a0b2f6111e8416d7a5166 100644 --- a/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java +++ b/client/src/test/java/dev/fitko/fitconnect/client/SubscriberClientTest.java @@ -3,9 +3,20 @@ package dev.fitko.fitconnect.client; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.jwk.RSAKey; +import dev.fitko.fitconnect.api.config.ApplicationConfig; +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.EventPayload; +import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.AttachmentEncryptionIssue; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.IncorrectAttachmentAuthenticationTag; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.MissingAttachment; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataEncryptionIssue; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataHashMismatch; +import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.IncorrectMetadataAuthenticationTag; import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog; +import dev.fitko.fitconnect.api.domain.model.event.problems.submission.MissingAuthenticationTags; +import dev.fitko.fitconnect.api.domain.model.event.problems.submission.NotExactlyOneSubmitEvent; 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; @@ -17,22 +28,27 @@ import dev.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.AuthenticationTagsEmptyException; import dev.fitko.fitconnect.api.exceptions.DecryptionException; import dev.fitko.fitconnect.api.exceptions.EventLogException; import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.exceptions.SubmissionRequestException; +import dev.fitko.fitconnect.api.exceptions.SubmitEventNotFoundException; import dev.fitko.fitconnect.api.exceptions.ValidationException; import dev.fitko.fitconnect.api.services.Subscriber; import dev.fitko.fitconnect.api.services.crypto.CryptoService; -import dev.fitko.fitconnect.client.testutil.LogCaptor; +import dev.fitko.fitconnect.client.subscriber.SubmissionReceiver; import dev.fitko.fitconnect.core.crypto.HashService; import dev.fitko.fitconnect.core.crypto.JWECryptoService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import org.springframework.http.HttpStatus; import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.text.ParseException; import java.util.Collections; @@ -47,10 +63,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; 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; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.times; @@ -60,7 +76,6 @@ import static org.mockito.Mockito.when; class SubscriberClientTest { private static final ObjectMapper mapper = new ObjectMapper(); - private final LogCaptor logs = new LogCaptor(); private Subscriber subscriberMock; private RSAKey privateKey; @@ -68,9 +83,10 @@ class SubscriberClientTest { @BeforeEach public void setup() throws IOException, ParseException { + final ApplicationConfig config = new ApplicationConfig(); privateKey = RSAKey.parse(getResourceAsString("private_decryption_test_key.json")); subscriberMock = Mockito.mock(Subscriber.class); - underTest = new SubscriberClient(subscriberMock, privateKey); + underTest = new SubscriberClient(subscriberMock, new SubmissionReceiver(subscriberMock, privateKey, config)); } @Test @@ -79,7 +95,7 @@ class SubscriberClientTest { assertThat(underTest.getAvailableSubmissionsForDestination(UUID.randomUUID()).isEmpty(), is(true)); final var destinationId = UUID.randomUUID(); - when(subscriberMock.pollAvailableSubmissionsForDestination(destinationId, 0, 5)).thenReturn(Collections.EMPTY_SET); + when(subscriberMock.pollAvailableSubmissionsForDestination(destinationId, 0, 5)).thenReturn(Collections.emptySet()); assertThat(underTest.getAvailableSubmissionsForDestination(destinationId).isEmpty(), is(true)); } @@ -89,31 +105,35 @@ class SubscriberClientTest { when(subscriberMock.getSubmission(any())).thenThrow(new RestApiException("Submission not found")); // When - final var receivedSubmission = underTest.requestSubmission(UUID.randomUUID()); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(UUID.randomUUID())); // Then - assertNull(receivedSubmission); - logs.assertContains("API request failed"); + assertThat(exception.getMessage(), containsString("Submission not found")); } @Test void testDecryptionFailed() { // Given final var submissionId = UUID.randomUUID(); + final var submission = new Submission(); submission.setSubmissionId(submissionId); submission.setEncryptedData("encryptedData"); submission.setEncryptedMetadata("encryptedMetadata"); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTags"); + authenticationTags.setData("dataAuthTags"); + when(subscriberMock.getSubmission(any())).thenReturn(submission); + when(subscriberMock.getAuthenticationTagsForEvent(Event.SUBMIT, submission)).thenReturn(authenticationTags); when(subscriberMock.decryptStringContent(any(), any())).thenThrow(new DecryptionException("Decryption failed")); // When - final var receivedSubmission = underTest.requestSubmission(submissionId); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId)); // Then - assertNull(receivedSubmission); - logs.assertContains("Decrypting payload failed"); + assertThat(exception.getMessage(), containsString("Decryption failed")); } @Test @@ -125,6 +145,7 @@ class SubscriberClientTest { submission.setSubmissionId(submissionId); submission.setCaseId(UUID.randomUUID()); submission.setDestinationId(UUID.randomUUID()); + submission.setEncryptedMetadata("abc"); final var hash = new Hash(); hash.setContent("hashedTestContent"); @@ -143,10 +164,18 @@ class SubscriberClientTest { final var metadata = new Metadata(); metadata.setContentStructure(contentStructure); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("abc"); + authenticationTags.setData("def"); + when(subscriberMock.getSubmission(any())).thenReturn(submission); when(subscriberMock.decryptStringContent(any(), any())).thenReturn(mapper.writeValueAsBytes(metadata)); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.validateAttachments(any(), any())).thenReturn(ValidationResult.ok()); + // When underTest.requestSubmission(submissionId).acceptSubmission(); @@ -164,6 +193,7 @@ class SubscriberClientTest { submission.setSubmissionId(submissionId); submission.setCaseId(UUID.randomUUID()); submission.setDestinationId(UUID.randomUUID()); + submission.setEncryptedMetadata("abc"); final var hash = new Hash(); hash.setContent("hashedTestContent"); @@ -181,16 +211,24 @@ class SubscriberClientTest { final var metadata = new Metadata(); metadata.setContentStructure(contentStructure); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("abc"); + authenticationTags.setData("def"); + when(subscriberMock.getSubmission(any())).thenReturn(submission); when(subscriberMock.decryptStringContent(any(), any())).thenReturn(mapper.writeValueAsBytes(metadata)); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.validateAttachments(any(), any())).thenReturn(ValidationResult.ok()); + // When - underTest.requestSubmission(submissionId).rejectSubmission(List.of(new InvalidEventLog())); + underTest.requestSubmission(submissionId).rejectSubmission(List.of(new DataEncryptionIssue())); // Then - final var expectedEventPayload = EventPayload.forRejectEvent(submission, List.of(new InvalidEventLog())); + final var expectedEventPayload = EventPayload.forRejectEvent(submission, List.of(new DataEncryptionIssue())); verify(subscriberMock, times(1)).rejectSubmission(expectedEventPayload); } @@ -198,20 +236,25 @@ class SubscriberClientTest { void testReadingMetadataFailed() { // Given final var submissionId = UUID.randomUUID(); + final var submission = new Submission(); submission.setSubmissionId(submissionId); submission.setEncryptedData("encryptedData"); submission.setEncryptedMetadata("encryptedMetadata"); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTags"); + authenticationTags.setData("dataAuthTags"); + when(subscriberMock.getSubmission(any())).thenReturn(submission); + when(subscriberMock.getAuthenticationTagsForEvent(Event.SUBMIT, submission)).thenReturn(authenticationTags); when(subscriberMock.decryptStringContent(any(), any())).thenReturn("encryptedMetadata".getBytes()); // When - final var receivedSubmission = underTest.requestSubmission(submissionId); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId)); // Then - assertNull(receivedSubmission); - logs.assertContains("Reading metadata failed"); + assertThat(exception.getMessage(), containsString("Unrecognized token 'encryptedMetadata'")); } @Test @@ -273,11 +316,18 @@ class SubscriberClientTest { submission.setEncryptedData(encryptedData); submission.setEncryptedMetadata(encryptedMetadata); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setData(encryptedData.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + authenticationTags.setMetadata(encryptedMetadata.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + when(subscriberMock.getSubmission(any())).thenReturn(submission); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateAttachments(any(), any())).thenReturn(ValidationResult.ok()); when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedData())).thenReturn(dataPayload.getBytes()); when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedMetadata())).thenReturn(metadataBytes); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); // When final var receivedSubmission = underTest.requestSubmission(submissionId); @@ -295,14 +345,14 @@ class SubscriberClientTest { when(subscriberMock.getSubmission(any())).thenThrow(new RestApiException("Submission not found")); // When - final var receivedSubmission = underTest.requestSubmission(UUID.randomUUID()); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(UUID.randomUUID())); // Then - assertNull(receivedSubmission); + assertThat(exception.getMessage(), containsString("Submission not found")); } @Test - void testInvalidMetadata() throws JsonProcessingException { + void testMetadataValidationFailed() throws JsonProcessingException { // Given final var submissionId = UUID.randomUUID(); final var destinationId = UUID.randomUUID(); @@ -312,19 +362,27 @@ class SubscriberClientTest { submission.setSubmissionId(submissionId); submission.setDestinationId(destinationId); submission.setCaseId(caseId); + submission.setEncryptedMetadata("abc"); + + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("abc"); final var invalidMetadata = mapper.writeValueAsBytes(new Metadata()); when(subscriberMock.getSubmission(any())).thenReturn(submission); when(subscriberMock.decryptStringContent(any(), any())).thenReturn(invalidMetadata); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.error(new ValidationException("Validation failed"))); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.error(new ValidationException("Metadata does not match schema"))); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + + final IncorrectMetadataAuthenticationTag problem = new IncorrectMetadataAuthenticationTag(); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.problem(problem)); // When - final var receivedSubmission = underTest.requestSubmission(submissionId); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId)); // Then - assertNull(receivedSubmission); - logs.assertContains("Metadata does not match schema"); + assertThat(exception.getMessage(), containsString("Metadata is invalid")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission, List.of(problem))); } @Test @@ -338,6 +396,7 @@ class SubscriberClientTest { submission.setSubmissionId(submissionId); submission.setDestinationId(destinationId); submission.setCaseId(caseId); + submission.setEncryptedMetadata("abc"); final Map<String, Map<?, ?>> metadata = Map.of( "$schemaFoo", Collections.emptyMap(), @@ -345,7 +404,7 @@ class SubscriberClientTest { "data", Map.of( "hash", Map.of( "type", SignatureType.SHA_512, - "content", "bla" + "content", "foo" ), "submissionSchema", Map.of( "schemaUri", URI.create("https://dummy.schema.url"), @@ -357,17 +416,24 @@ class SubscriberClientTest { final var invalidMetadata = mapper.writeValueAsBytes(metadata); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("abc"); + authenticationTags.setData("def"); + when(subscriberMock.getSubmission(any())).thenReturn(submission); when(subscriberMock.decryptStringContent(any(), any())).thenReturn(invalidMetadata); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.validateAttachments(any(), any())).thenReturn(ValidationResult.ok()); + // When final var receivedSubmission = underTest.requestSubmission(submissionId); // Then - assertNotNull(receivedSubmission); - assertThat(receivedSubmission.getMetadata().getContentStructure().getData().getHash().getContent(), is("bla")); + assertThat(receivedSubmission.getMetadata().getContentStructure().getData().getHash().getContent(), is("foo")); } @Test @@ -408,11 +474,18 @@ class SubscriberClientTest { submission.setEncryptedData(encryptedData); submission.setEncryptedMetadata(encryptedMetadata); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setData(encryptedData.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + authenticationTags.setMetadata(encryptedMetadata.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + when(subscriberMock.getSubmission(any())).thenReturn(submission); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.ok()); when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedData())).thenReturn(dataPayload.getBytes()); when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedMetadata())).thenReturn(metadataBytes); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.validateAttachments(any(), any())).thenReturn(ValidationResult.ok()); // When final var receivedSubmission = underTest.requestSubmission(submissionId); @@ -423,7 +496,7 @@ class SubscriberClientTest { } @Test - void testCorruptedData() throws JsonProcessingException { + void testDataValidationFailed() throws JsonProcessingException { // Given final RSAKey decryptionKey = privateKey; @@ -457,22 +530,32 @@ class SubscriberClientTest { submission.setEncryptedMetadata(encryptedMetadata); submission.setEncryptedData(encryptedData); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setData(encryptedData.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + authenticationTags.setMetadata(encryptedMetadata.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + when(subscriberMock.getSubmission(any())).thenReturn(submission); when(subscriberMock.decryptStringContent(decryptionKey, encryptedMetadata)).thenReturn(metadataBytes); when(subscriberMock.decryptStringContent(decryptionKey, encryptedData)).thenReturn(dataBytes); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); - when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.error(new ValidationException("Corrupt data hash"))); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + + final DataHashMismatch problem = new DataHashMismatch(); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.problem(problem)); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); // When - final var receivedSubmission = underTest.requestSubmission(submissionId); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId)); // Then - assertNull(receivedSubmission); - logs.assertContains("Data might be corrupted, hash-sum does not match"); + assertThat(exception.getMessage(), containsString("Data is invalid")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission, List.of(problem))); + } @Test - void testCorruptedAttachment() throws JsonProcessingException { + void testAttachmentValidationFailed() throws JsonProcessingException { // Given final RSAKey decryptionKey = privateKey; final RSAKey encryptionKey = decryptionKey.toPublicJWK(); @@ -486,6 +569,7 @@ class SubscriberClientTest { final var attachmentHash = new Hash(); attachmentHash.setContent(""); final var attachment = new ApiAttachment(); + attachment.setAttachmentId(UUID.randomUUID()); attachment.setFilename("src/test/resources/attachment.txt"); attachment.setHash(attachmentHash); @@ -511,21 +595,154 @@ class SubscriberClientTest { submission.setEncryptedMetadata(encryptedMetadata); submission.setEncryptedData(encryptedData); + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setData(encryptedData.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + authenticationTags.setMetadata(encryptedMetadata.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + when(subscriberMock.getSubmission(any())).thenReturn(submission); when(subscriberMock.decryptStringContent(decryptionKey, encryptedMetadata)).thenReturn(metadataBytes); when(subscriberMock.decryptStringContent(decryptionKey, encryptedData)).thenReturn(dataBytes); - when(subscriberMock.validateMetadata(any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); - final var decryptedDataBytes = cryptoService.decryptToBytes(decryptionKey, submission.getEncryptedData()); - when(subscriberMock.validateHashIntegrity(dataHash.getContent(), decryptedDataBytes)).thenReturn(ValidationResult.ok()); - when(subscriberMock.validateHashIntegrity(attachmentHash.getContent(), null)).thenReturn(ValidationResult.error(new ValidationException("Corrupt attachment"))); + final IncorrectAttachmentAuthenticationTag problem = new IncorrectAttachmentAuthenticationTag(attachment.getAttachmentId()); + when(subscriberMock.validateAttachments(anyList(), any())).thenReturn(ValidationResult.problem(problem)); // When - final var receivedSubmission = underTest.requestSubmission(submissionId); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId)); + + // Then + assertThat(exception.getMessage(), containsString("Attachment validation failed")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission, List.of(problem))); + } + + @Test + void testMissingAttachment() throws Exception { + + // Given + final var dataPayload = "{ some : 'data foo' }"; + final var submissionId = UUID.randomUUID(); + final CryptoService cryptoService = new JWECryptoService(new HashService()); + + final Hash hash = new Hash(); + hash.setContent(cryptoService.hashBytes(dataPayload.getBytes())); + hash.setSignatureType(SignatureType.SHA_512); + + final SubmissionSchema schema = new SubmissionSchema(); + schema.setSchemaUri(URI.create("https://dummy.schema.url")); + schema.setMimeType(MimeType.APPLICATION_JSON); + + final Data data = new Data(); + data.setSubmissionSchema(schema); + data.setHash(hash); + + final ApiAttachment attachment = new ApiAttachment(); + attachment.setAttachmentId(UUID.randomUUID()); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setAttachments(List.of(attachment)); + contentStructure.setData(data); + + final Metadata metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + + final byte[] metadataBytes = mapper.writeValueAsBytes(metadata); + + final RSAKey encryptionKey = privateKey; + final RSAKey decryptionKey = encryptionKey.toPublicJWK(); + + final String encryptedData = cryptoService.encryptBytes(decryptionKey, dataPayload.getBytes(StandardCharsets.UTF_8)); + final String encryptedMetadata = cryptoService.encryptBytes(decryptionKey, metadataBytes); + + final var submission = new Submission(); + submission.setSubmissionId(submissionId); + submission.setEncryptedData(encryptedData); + submission.setEncryptedMetadata(encryptedMetadata); + + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata(encryptedMetadata.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + + when(subscriberMock.getSubmission(any())).thenReturn(submission); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedData())).thenReturn(dataPayload.getBytes()); + when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedMetadata())).thenReturn(metadataBytes); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.fetchAttachment(any(), any())).thenThrow(new RestApiException(HttpStatus.NOT_FOUND, "Attachment download failed")); + + // When + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId)); + + // Then + assertThat(exception.getMessage(), containsString("Attachment download failed")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission, List.of(new MissingAttachment(attachment.getAttachmentId())))); + } + + @Test + void testAttachmentEncryptionIssue() throws Exception { + + // Given + final var dataPayload = "{ some : 'data foo' }"; + final var submissionId = UUID.randomUUID(); + final CryptoService cryptoService = new JWECryptoService(new HashService()); + + final Hash hash = new Hash(); + hash.setContent(cryptoService.hashBytes(dataPayload.getBytes())); + hash.setSignatureType(SignatureType.SHA_512); + + final SubmissionSchema schema = new SubmissionSchema(); + schema.setSchemaUri(URI.create("https://dummy.schema.url")); + schema.setMimeType(MimeType.APPLICATION_JSON); + + final Data data = new Data(); + data.setSubmissionSchema(schema); + data.setHash(hash); + + final ApiAttachment attachment = new ApiAttachment(); + attachment.setAttachmentId(UUID.randomUUID()); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setAttachments(List.of(attachment)); + contentStructure.setData(data); + + final Metadata metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + + final byte[] metadataBytes = mapper.writeValueAsBytes(metadata); + + final RSAKey encryptionKey = privateKey; + final RSAKey decryptionKey = encryptionKey.toPublicJWK(); + + final String encryptedData = cryptoService.encryptBytes(decryptionKey, dataPayload.getBytes(StandardCharsets.UTF_8)); + final String encryptedMetadata = cryptoService.encryptBytes(decryptionKey, metadataBytes); + + final var submission = new Submission(); + submission.setSubmissionId(submissionId); + submission.setEncryptedData(encryptedData); + submission.setEncryptedMetadata(encryptedMetadata); + + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata(encryptedMetadata.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN)[4]); + + when(subscriberMock.getSubmission(any())).thenReturn(submission); + when(subscriberMock.validateMetadata(any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.validateHashIntegrity(any(), any())).thenReturn(ValidationResult.ok()); + when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedData())).thenReturn(dataPayload.getBytes()); + when(subscriberMock.decryptStringContent(encryptionKey, submission.getEncryptedMetadata())).thenReturn(metadataBytes); + when(subscriberMock.decryptStringContent(encryptionKey, "encrypt$dAtt@chm$nt")).thenThrow(new DecryptionException("Decrypting attachment failed")); + when(subscriberMock.getAuthenticationTagsForEvent(any(), any())).thenReturn(authenticationTags); + when(subscriberMock.fetchAttachment(any(), any())).thenReturn("encrypt$dAtt@chm$nt"); + + // When + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submissionId)); // Then - assertNull(receivedSubmission); - logs.assertContains("Attachment data for id null is corrupted"); + assertThat(exception.getMessage(), containsString("Decrypting attachment failed")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission, List.of(new AttachmentEncryptionIssue(attachment.getAttachmentId())))); } @Test @@ -547,20 +764,63 @@ class SubscriberClientTest { } @Test - void testGetInvalidEventLog() { + void testNotExactlyOneSubmitEvent() { // Given - final var destinationId = UUID.randomUUID(); - final var caseId = UUID.randomUUID(); + final var submission = new Submission(); + submission.setSubmissionId(UUID.randomUUID()); + + final var expectedProblem = new NotExactlyOneSubmitEvent(); + + when(subscriberMock.getSubmission(submission.getSubmissionId())).thenReturn(submission); + when(subscriberMock.getAuthenticationTagsForEvent(Event.SUBMIT, submission)).thenThrow(new SubmitEventNotFoundException("no submit event in log")); + + // When + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submission.getSubmissionId())); + + // Then + assertThat(exception.getMessage(), containsString("The Event-Log is inconsistent because it does not contain exactly one 'submit' event.")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission,List.of(expectedProblem))); + } - when(subscriberMock.getEventLog(any(), any())).thenThrow(new EventLogException("Invalid log")); + @Test + void testMissingAuthenticationTags() { + + // Given + final var submission = new Submission(); + submission.setSubmissionId(UUID.randomUUID()); + + final var expectedProblem = new MissingAuthenticationTags(); + + when(subscriberMock.getSubmission(submission.getSubmissionId())).thenReturn(submission); + when(subscriberMock.getAuthenticationTagsForEvent(Event.SUBMIT, submission)).thenThrow(new AuthenticationTagsEmptyException("empty auth tags")); // When - final EventLogException exception = assertThrows(EventLogException.class, () -> underTest.getEventLog(caseId, destinationId)); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submission.getSubmissionId())); // Then - assertThat(exception.getMessage(), containsString("Invalid log")); + assertThat(exception.getMessage(), containsString("The 'submit-submission' event does not contain authentication tags")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission,List.of(expectedProblem))); + } + + @Test + void testInvalidEventLog() { + // Given + final var submission = new Submission(); + submission.setSubmissionId(UUID.randomUUID()); + + final InvalidEventLog expectedProblem = new InvalidEventLog(); + + when(subscriberMock.getSubmission(submission.getSubmissionId())).thenReturn(submission); + when(subscriberMock.getAuthenticationTagsForEvent(Event.SUBMIT, submission)).thenThrow(new EventLogException("Invalid log")); + + // When + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> underTest.requestSubmission(submission.getSubmissionId())); + + // Then + assertThat(exception.getMessage(), containsString("The Event-Log is inconsistent")); + verify(subscriberMock, times(1)).rejectSubmission(EventPayload.forRejectEvent(submission,List.of(expectedProblem))); } @Test diff --git a/client/src/test/resources/private_decryption_test_key.json b/client/src/test/resources/private_decryption_test_key.json index 9be02d7261d9283ac546d8e3eaee54deb84693e9..c5c076ebf299198b47a5f91e457821d52ef36c88 100644 --- a/client/src/test/resources/private_decryption_test_key.json +++ b/client/src/test/resources/private_decryption_test_key.json @@ -1 +1,16 @@ -{"alg":"RSA-OAEP-256","d":"UuZ7DtiI46pxCeACoP6cDxWBT2PJea6Td5eTU2sm-jkGH1O_ShgBtqHI6KwOLenO0Z1GlHoVNzIx9NeazUpVdN1Oan42ximA6LXqP6GaaWD7qJmzuSER968F4StW7hv5aI1N35mFLESY2ENISfhmRGBsCalRrQVFU5lRwjH22m_JTdeMeRbCCZHxynH2GcOwei8lZgWTiu3cIz7giEwc3GrZHZMxhdBLBCJ3gxDer9O_V0UCr7W7VpLpFuaOZmKaB2io-Rgz5W71IIo-09IghWnV5VZWpXr1Dl1_UbhE9UK_N6gQ9HcFopBQR6huD2svUO2aVig3RLO1KG8AL6vQsp8uIa_7_TuVYnuo7S_zVRlungjs2UzABjRqsdUED9C-FmwpnbKcHA1V99U8Ag22VHXm0zcT58M09-_yUl9s4VG6y-yQEhfxTjL0LMtSL8XScRwf38mpgX54LFGOeImo8d6jFGbNMzMViStejmXU8nCBeRmEtHpDU-GxJSPhO0ZJNgNWH_RU-tmujodXxXvyp7yA3MFqvcKxUQGf73lz_y10lLSmAJSlG-3KYyHY1N5AlXF4m0N3yENcim3VPVZarpOgHfEF3gg3slthAFZqTeY2WKMBp2O_QBvyf3mylVKHECzY4XzScBiPA9qlbeRDoa-NxcQwT2bS_1cyk8_IYG8","dp":"-sRGGYORpYDRglqvK9RX6FCQUpjCwbyAeq3yU4sEnC2XWt37-bw-p0KzSs7OuTTSPNH5I9HrfAzVHPzGXWE_CJmkdvkGqLkmvFTFiuGY1yk3psYvLwzDPJjyKUzdI-GMdioL_IyjEfna2TxdiYLyzGCDtGJxrSVyunc2xcMx_k1u_HQcylog00P61kCnUVtGsDqMbYsShCQaZXwQK3wPL6KkjgVLtEYYYeP8NiJRL_XP8SCD4ebU_cJMLDWgq1DgxpfqIBz0fCNe0ty9LwEhMhmP6OpmU5mRAGzXNKOp_lYYEVmvRoEwcRyFdM1W4N61eu8EYGyfzTrH1gfG8FMLfQ","dq":"uC9tAhQI7vRE2isCeoxY3YgJd2RBOdRrIjowUoS7WuEho_aLKWgoJDSzZoXad9pindjaSRj9CRKZ7oRW1Dp0NQwTQdUsyZSZt4cB5yxg50ty5IE81XDX0pgyRCJOXNpKxSZYtcBUzqlId4dCRd2codqaKJkP77K0TmWlQve_CoCPuaSP4CGqt8GpQj1fKzI_NYYqNVk-1jSAa9PZ2E-gW0iJwQoUNkDRTBgIVLx1qu7aYieYeif8oTFjKGKkP3EXCTGXKSdLd729-37NgWR0YktLFQsWfiNHcy1_-eWI6HKc8onOv5IuchKQ9nmQIE1mQUZBATCtx-vdzQqF_alTKw","e":"AQAB","key_ops":["unwrapKey"],"kid":"jsMQHFiA2uMGtiG3Ro8RJnYdEhp5W5KjGW-Vcf3-YMk","kty":"RSA","n":"zEi7NpGkzGyG2-PDWTy72hvti-pGBZLidxbk10_fenjzOavKcO5yGXSCfX_Xl0-WYaJfb7Kz6CRRtnwBGx8mrsftodtxt3kdrFnf6chQ2JJ5dmnz_ErIbHjHaFlxXvEqv3ivqIQSZvuns8QJip8RGQ63g7nmPDyvBcvLMFUtnXy7Y6rzUI4HO2eeg8htMPCWN5-L7Ol5_IT11NuZK6J3_UN1B0P7fFGdVMhsNR5D_jHOD-U4jZQijyVgzIXxwN_vnf90e5_ZUgsLypwh2DT7qzhqES8hzIIk_Cjs5mCUwDjfiBh0g7LNjXaj0b7rAn2X6yPuMK6zYi_FXJYSCQ7LH3THi_h5r2--xQ2h40He23JjfNpEGu98uMgB_dvzH0Dco8OKh4Aj2wjLpZVFOS3Zpu-WUalP-ecEiZ6nzmCMoIpWM0U4t4tpGwC1X-MWNj75zYLI8-yd3ELVBx3PQSzDukGOVLBdtpF0txf7Np0i9RFRW-nxf3xPVFCbZYbc87GMZy1CeDT2NBJJHtQaPKBismnEXtXtiM7aJDRM74F5JXjDVR62eHGxFCy46oyI_NS3FmvkoVtV8hvbXBRMSU8sNeOrVKnbWpfjBGFGD8VbssKoAAdMfn6usOiiH71SBg-L9KoBwdRfu7XFE-WbSA2d03hMvEPJWk9srh5qGdtQ_QE","p":"_RRumCEIUoM1gipAOIeQZ_td0E2qx20bFHArBf1V7ev4v4S22tt1Bt9oa20KPfA6mokycaB8MBCX7LTRvBT1XwSh4sU2TptuibElrarQvFoJIVGqugqG4GxsBpZpy-Dvx9NJtMTjiiGXSn87-_8iSWrPlc4Vdbgb8S9Vz2x_Mp-uSwZaFOO3Gx4miD29y1ugE26hFF0RdgBDJnJEk71W9NG2D_MbnBBa3DrPesdU8rX08RurpV8C1TVUgelDxUBQUxrRXRaHOPbrwPBBRDOgzNlFiuzUdkw6qwlfhp9W1tn3j7uAnqMHxnaPhJJ51tFjd_7MgSawmmM415c4NWa5xw","q":"zqQpmsvtjeU486C2DZ_NjsDHanYXgfxiY3X25XpM0ojLrtIQWKUaFxlggJGSQzQ6QJqB-5sJu9EEE4DGZ3UYoakY_hzxv0uHOHrI20ohFgF5K3YQdqGRhC3Bo66ezpmD3FT1EPXQM2OLEAi82lUb7UrbP_DSU0BmYOd6kPd1a3FD2qm0DjA15IYW5vUHNisx79W3kt_8s7ZICjiVpvJTSa1TVIX3wPM0qLHOWbHEOswLENC4fZ7EFYkNr3MvtAOxLcJ_mi4zzhAkyGdKJbTQXt6bhD_gWL5K1iOFM5Vg_NRTjUMqndDK33KDYZhC-MexSR-IaN7MbP1Y787uFL5S9w","qi":"DHzvGmAC4OZIfvDBkqXrvrvBeqURfioiEN72SL6oPcBqof899UL50BVohU0a2IeydwLSSff1i0U0jfX8ss_YKnCw2MxH3eNdQ-JXAQp7sefYLiFKPlGQnaxCNL0Z3kDatPhJnfTHaJA_UKtuPtcIINK5lJH80Iqqg3rl1p5G84ujbIghcgJXGoRGjJfkpUQgarTwPzdScR8Xo-YmN4dNav2X76s5toTJbSc4wLh3z_vdgL4GTGv9jzRKBPJcv07SDOFz92VA2r2DiIPQxg1eAKq8MPwEQ_bkaqh6Sk57_-JT8CRN0vfZbHw_3jCc8ijqRQE-4HaKcVCAbgnTzgr8cw"} \ No newline at end of file +{ + "alg": "RSA-OAEP-256", + "d": "UuZ7DtiI46pxCeACoP6cDxWBT2PJea6Td5eTU2sm-jkGH1O_ShgBtqHI6KwOLenO0Z1GlHoVNzIx9NeazUpVdN1Oan42ximA6LXqP6GaaWD7qJmzuSER968F4StW7hv5aI1N35mFLESY2ENISfhmRGBsCalRrQVFU5lRwjH22m_JTdeMeRbCCZHxynH2GcOwei8lZgWTiu3cIz7giEwc3GrZHZMxhdBLBCJ3gxDer9O_V0UCr7W7VpLpFuaOZmKaB2io-Rgz5W71IIo-09IghWnV5VZWpXr1Dl1_UbhE9UK_N6gQ9HcFopBQR6huD2svUO2aVig3RLO1KG8AL6vQsp8uIa_7_TuVYnuo7S_zVRlungjs2UzABjRqsdUED9C-FmwpnbKcHA1V99U8Ag22VHXm0zcT58M09-_yUl9s4VG6y-yQEhfxTjL0LMtSL8XScRwf38mpgX54LFGOeImo8d6jFGbNMzMViStejmXU8nCBeRmEtHpDU-GxJSPhO0ZJNgNWH_RU-tmujodXxXvyp7yA3MFqvcKxUQGf73lz_y10lLSmAJSlG-3KYyHY1N5AlXF4m0N3yENcim3VPVZarpOgHfEF3gg3slthAFZqTeY2WKMBp2O_QBvyf3mylVKHECzY4XzScBiPA9qlbeRDoa-NxcQwT2bS_1cyk8_IYG8", + "dp": "-sRGGYORpYDRglqvK9RX6FCQUpjCwbyAeq3yU4sEnC2XWt37-bw-p0KzSs7OuTTSPNH5I9HrfAzVHPzGXWE_CJmkdvkGqLkmvFTFiuGY1yk3psYvLwzDPJjyKUzdI-GMdioL_IyjEfna2TxdiYLyzGCDtGJxrSVyunc2xcMx_k1u_HQcylog00P61kCnUVtGsDqMbYsShCQaZXwQK3wPL6KkjgVLtEYYYeP8NiJRL_XP8SCD4ebU_cJMLDWgq1DgxpfqIBz0fCNe0ty9LwEhMhmP6OpmU5mRAGzXNKOp_lYYEVmvRoEwcRyFdM1W4N61eu8EYGyfzTrH1gfG8FMLfQ", + "dq": "uC9tAhQI7vRE2isCeoxY3YgJd2RBOdRrIjowUoS7WuEho_aLKWgoJDSzZoXad9pindjaSRj9CRKZ7oRW1Dp0NQwTQdUsyZSZt4cB5yxg50ty5IE81XDX0pgyRCJOXNpKxSZYtcBUzqlId4dCRd2codqaKJkP77K0TmWlQve_CoCPuaSP4CGqt8GpQj1fKzI_NYYqNVk-1jSAa9PZ2E-gW0iJwQoUNkDRTBgIVLx1qu7aYieYeif8oTFjKGKkP3EXCTGXKSdLd729-37NgWR0YktLFQsWfiNHcy1_-eWI6HKc8onOv5IuchKQ9nmQIE1mQUZBATCtx-vdzQqF_alTKw", + "e": "AQAB", + "key_ops": [ + "unwrapKey" + ], + "kid": "jsMQHFiA2uMGtiG3Ro8RJnYdEhp5W5KjGW-Vcf3-YMk", + "kty": "RSA", + "n": "zEi7NpGkzGyG2-PDWTy72hvti-pGBZLidxbk10_fenjzOavKcO5yGXSCfX_Xl0-WYaJfb7Kz6CRRtnwBGx8mrsftodtxt3kdrFnf6chQ2JJ5dmnz_ErIbHjHaFlxXvEqv3ivqIQSZvuns8QJip8RGQ63g7nmPDyvBcvLMFUtnXy7Y6rzUI4HO2eeg8htMPCWN5-L7Ol5_IT11NuZK6J3_UN1B0P7fFGdVMhsNR5D_jHOD-U4jZQijyVgzIXxwN_vnf90e5_ZUgsLypwh2DT7qzhqES8hzIIk_Cjs5mCUwDjfiBh0g7LNjXaj0b7rAn2X6yPuMK6zYi_FXJYSCQ7LH3THi_h5r2--xQ2h40He23JjfNpEGu98uMgB_dvzH0Dco8OKh4Aj2wjLpZVFOS3Zpu-WUalP-ecEiZ6nzmCMoIpWM0U4t4tpGwC1X-MWNj75zYLI8-yd3ELVBx3PQSzDukGOVLBdtpF0txf7Np0i9RFRW-nxf3xPVFCbZYbc87GMZy1CeDT2NBJJHtQaPKBismnEXtXtiM7aJDRM74F5JXjDVR62eHGxFCy46oyI_NS3FmvkoVtV8hvbXBRMSU8sNeOrVKnbWpfjBGFGD8VbssKoAAdMfn6usOiiH71SBg-L9KoBwdRfu7XFE-WbSA2d03hMvEPJWk9srh5qGdtQ_QE", + "p": "_RRumCEIUoM1gipAOIeQZ_td0E2qx20bFHArBf1V7ev4v4S22tt1Bt9oa20KPfA6mokycaB8MBCX7LTRvBT1XwSh4sU2TptuibElrarQvFoJIVGqugqG4GxsBpZpy-Dvx9NJtMTjiiGXSn87-_8iSWrPlc4Vdbgb8S9Vz2x_Mp-uSwZaFOO3Gx4miD29y1ugE26hFF0RdgBDJnJEk71W9NG2D_MbnBBa3DrPesdU8rX08RurpV8C1TVUgelDxUBQUxrRXRaHOPbrwPBBRDOgzNlFiuzUdkw6qwlfhp9W1tn3j7uAnqMHxnaPhJJ51tFjd_7MgSawmmM415c4NWa5xw", + "q": "zqQpmsvtjeU486C2DZ_NjsDHanYXgfxiY3X25XpM0ojLrtIQWKUaFxlggJGSQzQ6QJqB-5sJu9EEE4DGZ3UYoakY_hzxv0uHOHrI20ohFgF5K3YQdqGRhC3Bo66ezpmD3FT1EPXQM2OLEAi82lUb7UrbP_DSU0BmYOd6kPd1a3FD2qm0DjA15IYW5vUHNisx79W3kt_8s7ZICjiVpvJTSa1TVIX3wPM0qLHOWbHEOswLENC4fZ7EFYkNr3MvtAOxLcJ_mi4zzhAkyGdKJbTQXt6bhD_gWL5K1iOFM5Vg_NRTjUMqndDK33KDYZhC-MexSR-IaN7MbP1Y787uFL5S9w", + "qi": "DHzvGmAC4OZIfvDBkqXrvrvBeqURfioiEN72SL6oPcBqof899UL50BVohU0a2IeydwLSSff1i0U0jfX8ss_YKnCw2MxH3eNdQ-JXAQp7sefYLiFKPlGQnaxCNL0Z3kDatPhJnfTHaJA_UKtuPtcIINK5lJH80Iqqg3rl1p5G84ujbIghcgJXGoRGjJfkpUQgarTwPzdScR8Xo-YmN4dNav2X76s5toTJbSc4wLh3z_vdgL4GTGv9jzRKBPJcv07SDOFz92VA2r2DiIPQxg1eAKq8MPwEQ_bkaqh6Sk57_-JT8CRN0vfZbHw_3jCc8ijqRQE-4HaKcVCAbgnTzgr8cw" +} \ 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 68711b85676591f5df52ea08e1c89797ff093ec3..00302464512b983ae1f7116a8bdbe620e9f11069 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSender.java @@ -47,12 +47,6 @@ public class SubmissionSender implements Sender { this.keyService = keyService; } - @Override - public ValidationResult validatePublicKey(final RSAKey publicKey) { - LOGGER.info("Validating public key integrity"); - return validationService.validateEncryptionPublicKey(publicKey); - } - @Override public ValidationResult validateMetadata(final Metadata metadata) { LOGGER.info("Validating metadata"); 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 ddc012016dfcdea8efba56b0699ea053009218eb..3de5c7e0b9df2423b624cacec996d3d624782392 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/SubmissionSubscriber.java @@ -2,12 +2,19 @@ package dev.fitko.fitconnect.core; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.SignedJWT; +import dev.fitko.fitconnect.api.domain.model.destination.Destination; +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.EventPayload; +import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation; import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.DecryptionException; +import dev.fitko.fitconnect.api.exceptions.EventCreationException; +import dev.fitko.fitconnect.api.exceptions.RestApiException; import dev.fitko.fitconnect.api.services.Subscriber; import dev.fitko.fitconnect.api.services.crypto.CryptoService; import dev.fitko.fitconnect.api.services.events.EventLogService; @@ -44,7 +51,7 @@ public class SubmissionSubscriber implements Subscriber { } @Override - public byte[] decryptStringContent(final RSAKey privateKey, final String encryptedContent) { + public byte[] decryptStringContent(final RSAKey privateKey, final String encryptedContent) throws DecryptionException { LOGGER.info("Decrypting string"); return cryptoService.decryptToBytes(privateKey, encryptedContent); } @@ -62,32 +69,50 @@ public class SubmissionSubscriber implements Subscriber { } @Override - public Submission getSubmission(final UUID submissionId) { + public Submission getSubmission(final UUID submissionId) throws RestApiException { LOGGER.info("Loading submission {}", submissionId); return submissionService.getSubmission(submissionId); } @Override - public String fetchAttachment(final UUID submissionId, final UUID attachmentId) { + public String fetchAttachment(final UUID submissionId, final UUID attachmentId) throws RestApiException { LOGGER.info("Loading attachment {} for submission {}", attachmentId, submissionId); return submissionService.getAttachment(submissionId, attachmentId); } @Override - public List<EventLogEntry> getEventLog(final UUID caseId, final UUID destinationId) { + public List<EventLogEntry> getEventLog(final UUID caseId, final UUID destinationId) throws RestApiException { LOGGER.info("Loading event log for destination {}", destinationId); return eventLogService.getEventLog(caseId, destinationId); } + @Override + public AuthenticationTags getAuthenticationTagsForEvent(final Event event, final Submission submission) { + LOGGER.info("Loading authentication tags of {} event for submission {}", event, submission.getSubmissionId()); + return eventLogService.getAuthenticationTagsForEvent(event, submission); + } + + @Override + public ValidationResult validateMetadata(final Metadata metadata, final Submission submission, final AuthenticationTags authenticationTags) { + LOGGER.info("Validating metadata"); + final Destination destination = submissionService.getDestination(submission.getDestinationId()); + return validationService.validateMetadata(metadata, submission, destination, authenticationTags); + } + + @Override + public ValidationResult validateData(final byte[] data, final Submission submission, final Metadata metadata, final AuthenticationTags authenticationTags) { + LOGGER.info("Validating data"); + return validationService.validateData(data, submission, metadata, authenticationTags); + } @Override - public ValidationResult validateMetadata(final Metadata metadata) { - LOGGER.info("Validating metadata schema"); - return validationService.validateMetadataSchema(metadata); + public ValidationResult validateAttachments(final List<AttachmentForValidation> attachmentsForValidation, final AuthenticationTags authenticationTags) { + LOGGER.info("Validating attachments"); + return validationService.validateAttachments(attachmentsForValidation, authenticationTags); } @Override public ValidationResult validateHashIntegrity(final String originalHash, final byte[] data) { - LOGGER.info("Validating data hash integrity"); + LOGGER.info("Validating hash integrity"); return validationService.validateHashIntegrity(originalHash, data); } @@ -98,7 +123,7 @@ public class SubmissionSubscriber implements Subscriber { } @Override - public void acceptSubmission(final EventPayload eventPayload) { + public void acceptSubmission(final EventPayload eventPayload) throws RestApiException, EventCreationException { LOGGER.info("Accepting submission"); final SignedJWT confirmedSubmissionEvent = securityEventService.createAcceptSubmissionEvent(eventPayload); eventLogService.sendEvent(eventPayload.getCaseId(), confirmedSubmissionEvent.serialize()); @@ -106,7 +131,7 @@ public class SubmissionSubscriber implements Subscriber { } @Override - public void rejectSubmission(final EventPayload eventPayload) { + public void rejectSubmission(final EventPayload eventPayload) throws RestApiException, EventCreationException { LOGGER.info("Rejecting submission"); final SignedJWT rejectSubmissionEvent = securityEventService.createRejectSubmissionEvent(eventPayload); eventLogService.sendEvent(eventPayload.getCaseId(), rejectSubmissionEvent.serialize()); diff --git a/core/src/main/java/dev/fitko/fitconnect/core/crypto/JWECryptoService.java b/core/src/main/java/dev/fitko/fitconnect/core/crypto/JWECryptoService.java index b063236ba28c92ff979131826681e6e7daa57bfd..e43b59f16f73efdedac896c74805bb1d34717424 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/crypto/JWECryptoService.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/crypto/JWECryptoService.java @@ -2,7 +2,13 @@ package dev.fitko.fitconnect.core.crypto; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.*; +import com.nimbusds.jose.CompressionAlgorithm; +import com.nimbusds.jose.EncryptionMethod; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEAlgorithm; +import com.nimbusds.jose.JWEHeader; +import com.nimbusds.jose.JWEObject; +import com.nimbusds.jose.Payload; import com.nimbusds.jose.crypto.RSADecrypter; import com.nimbusds.jose.crypto.RSAEncrypter; import com.nimbusds.jose.jwk.RSAKey; @@ -78,7 +84,7 @@ public class JWECryptoService implements CryptoService { jwe.decrypt(new RSADecrypter(privateKey)); LOGGER.info("Decrypting {} bytes took {} ", encData.getBytes().length, StopWatch.stopWithFormattedTime(start)); return jwe.getPayload(); - } catch (final ParseException | JOSEException e) { + } catch (final ParseException | IllegalStateException | JOSEException e) { throw new DecryptionException(e.getMessage(), e); } } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java index cd211de6cab748b1bf27484468c68ae915cbb043..c6e49cbb155f6980f66e8908378c67077e6e8d05 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/events/EventLogApiService.java @@ -2,18 +2,20 @@ package dev.fitko.fitconnect.core.events; import com.nimbusds.jwt.SignedJWT; import dev.fitko.fitconnect.api.config.ApplicationConfig; +import dev.fitko.fitconnect.api.domain.model.event.Event; import dev.fitko.fitconnect.api.domain.model.event.EventLog; import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry; import dev.fitko.fitconnect.api.domain.model.event.SubmissionStatus; import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; +import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.validation.ValidationContext; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; +import dev.fitko.fitconnect.api.exceptions.AuthenticationTagsEmptyException; import dev.fitko.fitconnect.api.exceptions.EventLogException; -import dev.fitko.fitconnect.api.exceptions.RestApiException; +import dev.fitko.fitconnect.api.exceptions.SubmitEventNotFoundException; import dev.fitko.fitconnect.api.services.auth.OAuthService; import dev.fitko.fitconnect.api.services.events.EventLogService; import dev.fitko.fitconnect.api.services.events.EventLogVerificationService; -import dev.fitko.fitconnect.core.util.EventLogUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; @@ -28,6 +30,13 @@ import java.util.List; import java.util.UUID; import java.util.stream.Collectors; +import static dev.fitko.fitconnect.core.util.EventLogUtil.eventEqualsSubmissionId; +import static dev.fitko.fitconnect.core.util.EventLogUtil.eventToEntry; +import static dev.fitko.fitconnect.core.util.EventLogUtil.getAuthenticationTags; +import static dev.fitko.fitconnect.core.util.EventLogUtil.getJWTSFromEvents; +import static dev.fitko.fitconnect.core.util.EventLogUtil.getJwtsFromEvents; +import static dev.fitko.fitconnect.core.util.EventLogUtil.mapEventLogToEntries; + public class EventLogApiService implements EventLogService { private static final Logger LOGGER = LoggerFactory.getLogger(EventLogApiService.class); @@ -48,37 +57,59 @@ public class EventLogApiService implements EventLogService { } @Override - public List<EventLogEntry> getEventLog(final UUID caseId, final UUID destinationId) { + public List<EventLogEntry> getEventLog(final UUID caseId, final UUID destinationId) throws EventLogException { final EventLog eventLog = loadEventLog(caseId); - final List<SignedJWT> events = EventLogUtil.getJWTSFromEvents(eventLog.getEventLogs()); + final List<SignedJWT> events = getJWTSFromEvents(eventLog.getEventLogs()); final ValidationContext ctx = new ValidationContext(destinationId, caseId); - validateLog(eventLogVerifier.validateEventLogs(ctx, events)); - return EventLogUtil.mapEventLogToEntries(eventLog); + evaluateLogValidation(eventLogVerifier.validateEventLogs(ctx, events)); + return mapEventLogToEntries(eventLog); + } + + @Override + public AuthenticationTags getAuthenticationTagsForEvent(final Event event, final Submission submission) throws EventLogException { + + final EventLog eventLog = loadEventLog(submission.getCaseId()); + final List<SignedJWT> submitEvents = getJwtsFromEvents(event, submission, eventLog); + + if (submitEvents.size() != 1) { + throw new SubmitEventNotFoundException("Event log does not contain exactly one submit event"); + } + + final SignedJWT submitEvent = submitEvents.stream().findFirst().get(); + final AuthenticationTags authenticationTags = getAuthenticationTags(submitEvent); + + if(authenticationTags.getMetadata() == null && authenticationTags.getData() == null){ + throw new AuthenticationTagsEmptyException("Authentication tags are empty"); + } + + verifyEventWithAuthTags(submission, submitEvent, authenticationTags); + + return authenticationTags; } @Override - public SubmissionStatus getLastedEvent(final UUID destinationId, final UUID caseId, final UUID submissionId, final AuthenticationTags authenticationTags) throws RestApiException, EventLogException { + public SubmissionStatus getLastedEvent(final UUID destinationId, final UUID caseId, final UUID submissionId, final AuthenticationTags authenticationTags) throws EventLogException { final EventLog eventLog = loadEventLog(caseId); - final SignedJWT latestEvent = getLatestEventForSubmission(submissionId, eventLog); + final SignedJWT latestEvent = getLatestEventForSubmission(submissionId, eventLog.getEventLogs()); if (latestEvent == null) { LOGGER.info("No events found for submission {}", submissionId); return new SubmissionStatus(); } final var ctx = new ValidationContext(destinationId, caseId, authenticationTags); - validateLog(eventLogVerifier.validateEventLogs(ctx, List.of(latestEvent))); - final EventLogEntry eventLogEntry = EventLogUtil.eventToEntry(latestEvent); + evaluateLogValidation(eventLogVerifier.validateEventLogs(ctx, List.of(latestEvent))); + final EventLogEntry eventLogEntry = eventToEntry(latestEvent); return new SubmissionStatus(eventLogEntry.getEvent().getState(), eventLogEntry.getProblems()); } @Override - public void sendEvent(final UUID caseId, final String signedAndSerializedSET) { + public void sendEvent(final UUID caseId, final String signedAndSerializedSET) throws EventLogException { final String url = config.getEventsEndpoint(); final HttpHeaders headers = getHttpHeaders("application/jose"); final HttpEntity<String> entity = new HttpEntity<>(signedAndSerializedSET, headers); try { restTemplate.exchange(url, HttpMethod.POST, entity, Void.class, caseId); } catch (final RestClientException e) { - throw new RestApiException("Sending event failed", e); + throw new EventLogException("Sending event failed", e); } } @@ -89,7 +120,7 @@ public class EventLogApiService implements EventLogService { try { return restTemplate.exchange(url, HttpMethod.GET, entity, EventLog.class, caseId).getBody(); } catch (final RestClientException e) { - throw new RestApiException("EventLog query failed", e); + throw new EventLogException("EventLog query failed", e); } } @@ -109,21 +140,33 @@ public class EventLogApiService implements EventLogService { return headers; } - private SignedJWT getLatestEventForSubmission(final UUID submissionId, final EventLog eventLog) { - return EventLogUtil.getJWTSFromEvents(eventLog.getEventLogs()).stream() - .filter(EventLogUtil.eventEqualsSubmissionId(submissionId)) + private SignedJWT getLatestEventForSubmission(final UUID submissionId, final List<String> events) { + return getJWTSFromEvents(events).stream() + .filter(eventEqualsSubmissionId(submissionId)) .reduce((first, second) -> second) .orElse(null); } - private void validateLog(final List<ValidationResult> results) { - if (!results.isEmpty()) { - final String errorMessages = results.stream() + private void verifyEventWithAuthTags(final Submission submission, final SignedJWT submitEvent, final AuthenticationTags authenticationTags) { + + final var ctx = new ValidationContext(submission.getDestinationId(), submission.getCaseId(), authenticationTags); + final List<ValidationResult> validationResults = eventLogVerifier.validateEventLogs(ctx, List.of(submitEvent)); + + if(!validationResults.isEmpty()){ + final String errorMessages = validationResults.stream() .map(ValidationResult::getError) - .map(Throwable::getMessage) - .distinct() + .map(Exception::getMessage) .collect(Collectors.joining("\n")); - throw new EventLogException("Event log contains invalid entries: \n" + errorMessages); + throw new EventLogException(errorMessages); } } + + private void evaluateLogValidation(final List<ValidationResult> results) { + results.stream() + .filter(ValidationResult::hasError) + .findFirst() + .ifPresent((result) -> { + throw new EventLogException(result.getError().getMessage(), result.getError()); + }); + } } 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 905a5d52a3cb65adaa5753c0816da5b614863c53..b097f861baf262b3845c74a4030d166688f985c0 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 @@ -5,6 +5,7 @@ import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.KeyOperation; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; @@ -99,12 +100,12 @@ public class EventLogVerifier implements EventLogVerificationService { if (header.getAlgorithm() == null) { ctx.addError("The provided alg in the SET header must not be null."); } else { - ctx.addResult(header.getAlgorithm() == JWSAlgorithm.PS512, "The provided alg in the SET header is not allowed."); + ctx.addErrorIfTestFailed(header.getAlgorithm() == JWSAlgorithm.PS512, "The provided alg in the SET header is not allowed."); } if (header.getType() == null) { ctx.addError("The provided typ in the SET header must not be null."); } else { - ctx.addResult(header.getType().toString().equals(HEADER_TYPE), "The provided typ in the SET header is not " + HEADER_TYPE); + ctx.addErrorIfTestFailed(header.getType().toString().equals(HEADER_TYPE), "The provided typ in the SET header is not " + HEADER_TYPE); } } @@ -122,16 +123,16 @@ public class EventLogVerifier implements EventLogVerificationService { } private void validatePayload(final ValidationContext ctx, final JWTClaimsSet claims) throws ParseException { - ctx.addResult(claims.getClaim(ISSUER) != null, "The claim iss is missing in the payload of the SET."); - ctx.addResult(claims.getClaim(ISSUED_AT) != null, "The claim iat is missing in the payload of the SET."); - ctx.addResult(claims.getClaim(JWT_ID) != null, "The claim jti is missing in the payload of the SET."); - ctx.addResult(claims.getClaim(SUBJECT) != null, "The claim sub is missing in the payload of the SET."); - ctx.addResult(claims.getClaim(CLAIM_TXN) != null, "The claim txn is missing in the payload of the SET."); - ctx.addResult(claims.getClaim(CLAIM_EVENTS) != null, "The claim events is missing in the payload of the SET."); - ctx.addResult(claims.getJSONObjectClaim(CLAIM_EVENTS).keySet().size() == 1, "The events claims has must be exactly one event."); - ctx.addResult(claims.getStringClaim("sub").matches("(submission|case|reply):" + UUID_V4_PATTERN), "The provided subject does not match the allowed pattern."); - ctx.addResult(claims.getStringClaim(CLAIM_TXN).matches("case:" + UUID_V4_PATTERN), "The provided txn does not match the allowed pattern."); - getEventClaim(claims).ifPresentOrElse(event -> ctx.addResult(Event.fromSchemaUri(event) != null, "The provided event is not a valid event supported by this instance."), + ctx.addErrorIfTestFailed(claims.getClaim(ISSUER) != null, "The claim iss is missing in the payload of the SET."); + ctx.addErrorIfTestFailed(claims.getClaim(ISSUED_AT) != null, "The claim iat is missing in the payload of the SET."); + ctx.addErrorIfTestFailed(claims.getClaim(JWT_ID) != null, "The claim jti is missing in the payload of the SET."); + ctx.addErrorIfTestFailed(claims.getClaim(SUBJECT) != null, "The claim sub is missing in the payload of the SET."); + ctx.addErrorIfTestFailed(claims.getClaim(CLAIM_TXN) != null, "The claim txn is missing in the payload of the SET."); + ctx.addErrorIfTestFailed(claims.getClaim(CLAIM_EVENTS) != null, "The claim events is missing in the payload of the SET."); + ctx.addErrorIfTestFailed(claims.getJSONObjectClaim(CLAIM_EVENTS).keySet().size() == 1, "The events claims has must be exactly one event."); + ctx.addErrorIfTestFailed(claims.getStringClaim("sub").matches("(submission|case|reply):" + UUID_V4_PATTERN), "The provided subject does not match the allowed pattern."); + ctx.addErrorIfTestFailed(claims.getStringClaim(CLAIM_TXN).matches("case:" + UUID_V4_PATTERN), "The provided txn does not match the allowed pattern."); + getEventClaim(claims).ifPresentOrElse(event -> ctx.addErrorIfTestFailed(Event.fromSchemaUri(event) != null, "The provided event is not a valid event supported by this instance."), () -> ctx.addError("No events in JWT")); } @@ -139,7 +140,7 @@ public class EventLogVerifier implements EventLogVerificationService { if (signatureKey == null) { ctx.addError("The signature key cannot be validated, it must not be null."); } else { - ctx.addResult(validationService.validateSignaturePublicKey(signatureKey)); + ctx.addResult(validationService.validatePublicKey(signatureKey, KeyOperation.VERIFY)); } } @@ -148,7 +149,7 @@ public class EventLogVerifier implements EventLogVerificationService { ctx.addError("The signature cannot validated, signature key is unavailable."); } else { final JWSVerifier jwsVerifier = new RSASSAVerifier(signatureKey); - ctx.addResult(signedJWT.verify(jwsVerifier), "The signature of the token could not be verified with the specified key."); + ctx.addErrorIfTestFailed(signedJWT.verify(jwsVerifier), "The signature of the token could not be verified with the specified key."); } } @@ -177,15 +178,15 @@ public class EventLogVerifier implements EventLogVerificationService { ctx.addError("The event '" + event.getSchemaUri() + "' has to be created by the destination ('iss' claim must be an UUID)"); } if (destinationId != null) { - ctx.addResult(destinationId.equals(ctx.getDestinationId()), "The destination of the submission is not the issuer ('iss' claim must match submission.destinationId)"); + ctx.addErrorIfTestFailed(destinationId.equals(ctx.getDestinationId()), "The destination of the submission is not the issuer ('iss' claim must match submission.destinationId)"); } } private void validateAuthenticationTags(final ValidationContext ctx, final JWTClaimsSet payload) throws ParseException { final AuthenticationTags eventAuthTags = getAuthenticationTags(payload); final AuthenticationTags submissionAuthTags = ctx.getAuthenticationTags(); - ctx.addResult(eventAuthTags != null, "AuthenticationTags of event must not be null."); - ctx.addResult(submissionAuthTags != null, "AuthenticationTags of submission must not be null."); + ctx.addErrorIfTestFailed(eventAuthTags != null, "AuthenticationTags of event must not be null."); + ctx.addErrorIfTestFailed(submissionAuthTags != null, "AuthenticationTags of submission must not be null."); if (eventAuthTags != null && submissionAuthTags != null) { validateDataAuthTags(ctx, eventAuthTags.getData(), submissionAuthTags.getData()); validateMetadataAuthTags(ctx, eventAuthTags.getMetadata(), submissionAuthTags.getMetadata()); @@ -195,20 +196,20 @@ public class EventLogVerifier implements EventLogVerificationService { private void validateAttachmentsAuthTags(final ValidationContext ctx, final Map<UUID, String> eventAttachmentTags, final Map<UUID, String> submissionAttachmentTags) { if (eventAttachmentTags != null && submissionAttachmentTags != null) { - ctx.addResult(eventAttachmentTags.size() == submissionAttachmentTags.size(), "The events quantity of attachments does not match the submission."); + ctx.addErrorIfTestFailed(eventAttachmentTags.size() == submissionAttachmentTags.size(), "The events quantity of attachments does not match the submission."); eventAttachmentTags.forEach((key, eventTag) -> { final String submissionTag = submissionAttachmentTags.get(key); - ctx.addResult(eventTag.equals(submissionTag), "The authentication-tag for the attachment " + key + " does not match the submission."); + ctx.addErrorIfTestFailed(eventTag.equals(submissionTag), "The authentication-tag for the attachment " + key + " does not match the submission."); }); } } private void validateDataAuthTags(final ValidationContext ctx, final String eventDataAuthTag, final String submissionDataAuthTag) { - ctx.addResult(eventDataAuthTag.equals(submissionDataAuthTag), "The events data authentication-tag does not match the submission."); + ctx.addErrorIfTestFailed(eventDataAuthTag.equals(submissionDataAuthTag), "The events data authentication-tag does not match the submission."); } private void validateMetadataAuthTags(final ValidationContext ctx, final String eventMetadataAuthTag, final String submissionMetadataAuthTag) { - ctx.addResult(eventMetadataAuthTag.equals(submissionMetadataAuthTag), "The events metadata authentication-tag does not match the submission."); + ctx.addErrorIfTestFailed(eventMetadataAuthTag.equals(submissionMetadataAuthTag), "The events metadata authentication-tag does not match the submission."); } private static boolean eventContainsAuthTags(final JWTClaimsSet payload) throws ParseException { diff --git a/core/src/main/java/dev/fitko/fitconnect/core/events/SecurityEventTokenService.java b/core/src/main/java/dev/fitko/fitconnect/core/events/SecurityEventTokenService.java index c6054dc66a485d3ed0e45047cc986489b5112f68..0191811982550a7774f4cea128b76a09de31eeec 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/events/SecurityEventTokenService.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/events/SecurityEventTokenService.java @@ -21,6 +21,7 @@ import dev.fitko.fitconnect.api.services.events.SecurityEventService; import dev.fitko.fitconnect.api.services.validation.ValidationService; import java.text.ParseException; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -123,6 +124,9 @@ public class SecurityEventTokenService implements SecurityEventService { } private Map<UUID, String> buildAttachmentAuthenticationTags(final Map<UUID, String> encryptedAttachments) { + if(encryptedAttachments == null || encryptedAttachments.isEmpty()){ + return Collections.emptyMap(); + } return encryptedAttachments .entrySet() .stream() 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 2ab47f490335b6d9aee31e5e76e7274b0867de14..2445d98c39707200eb5bbc8c080cda126bce8d05 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 @@ -2,6 +2,7 @@ package dev.fitko.fitconnect.core.keys; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.jwk.KeyOperation; import com.nimbusds.jose.jwk.RSAKey; import dev.fitko.fitconnect.api.config.ApplicationConfig; import dev.fitko.fitconnect.api.domain.model.destination.Destination; @@ -107,12 +108,12 @@ public class PublicKeyService implements KeyService { } private void validateEncryptionKey(final RSAKey rsaKey) { - final ValidationResult result = validationService.validateEncryptionPublicKey(rsaKey); + final ValidationResult result = validationService.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY); validateResult(result, "Invalid public encryption key"); } private void validateSignatureKey(final RSAKey rsaKey) { - final ValidationResult result = validationService.validateSignaturePublicKey(rsaKey); + final ValidationResult result = validationService.validatePublicKey(rsaKey, KeyOperation.VERIFY); validateResult(result, "Public signature key is not valid"); } 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 d940c7ccbb2d78bc37f61003138e5b6b16ac2bf4..9af4798e4282d65266f8e255dc3dc8d54206efc1 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 @@ -19,7 +19,9 @@ import lombok.Setter; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -42,7 +44,7 @@ public class SubmissionApiService implements SubmissionService { } @Override - public Destination getDestination(final UUID destinationID) { + public Destination getDestination(final UUID destinationID) throws RestApiException { final RequestSettings requestSettings = RequestSettings.builder() .url(config.getDestinationsEndpoint()) .method(HttpMethod.GET) @@ -55,7 +57,7 @@ public class SubmissionApiService implements SubmissionService { } @Override - public SubmissionForPickup announceSubmission(final CreateSubmission submission) { + public SubmissionForPickup announceSubmission(final CreateSubmission submission) throws RestApiException { final RequestSettings requestSettings = RequestSettings.builder() .url(config.getSubmissionsEndpoint()) .method(HttpMethod.POST) @@ -68,7 +70,7 @@ public class SubmissionApiService implements SubmissionService { } @Override - public void uploadAttachment(final UUID submissionId, final UUID attachmentId, final String encryptedAttachment) { + public void uploadAttachment(final UUID submissionId, final UUID attachmentId, final String encryptedAttachment) throws RestApiException { final Map<String, Object> params = new HashMap<>(); params.put("submissionId", submissionId); params.put("attachmentId", attachmentId); @@ -84,7 +86,7 @@ public class SubmissionApiService implements SubmissionService { } @Override - public String getAttachment(final UUID submissionId, final UUID attachmentId) { + public String getAttachment(final UUID submissionId, final UUID attachmentId) throws RestApiException { final Map<String, Object> params = new HashMap<>(); params.put("submissionId", submissionId); params.put("attachmentId", attachmentId); @@ -100,7 +102,7 @@ public class SubmissionApiService implements SubmissionService { } @Override - public Submission sendSubmission(final SubmitSubmission submission) { + public Submission sendSubmission(final SubmitSubmission submission) throws RestApiException { final Map<String, Object> params = Map.of("submissionId", submission.getSubmissionId()); final RequestSettings requestSettings = RequestSettings.builder() .url(config.getSubmissionEndpoint()) @@ -114,7 +116,7 @@ public class SubmissionApiService implements SubmissionService { } @Override - public SubmissionsForPickup pollAvailableSubmissions(final int offset, final int limit) { + public SubmissionsForPickup pollAvailableSubmissions(final int offset, final int limit) throws RestApiException { final String urlWithQueryParams = UriComponentsBuilder.fromHttpUrl(config.getSubmissionsEndpoint()) .queryParam("limit", limit) .queryParam("offset", offset) @@ -149,7 +151,7 @@ public class SubmissionApiService implements SubmissionService { } @Override - public Submission getSubmission(final UUID submissionId) { + public Submission getSubmission(final UUID submissionId) throws RestApiException { final Map<String, Object> params = Map.of("submissionId", submissionId); final RequestSettings requestSettings = RequestSettings.builder() .url(config.getSubmissionEndpoint()) @@ -169,12 +171,19 @@ public class SubmissionApiService implements SubmissionService { final Class<T> responseType = requestSettings.responseType; final Map<String, ?> params = requestSettings.params; try { - return restTemplate.exchange(url, method, entity, responseType, params).getBody(); + return evaluateStatusCode(restTemplate.exchange(url, method, entity, responseType, params)); } catch (final RestClientException e) { throw new RestApiException(e.getMessage(), e); } } + private <T> T evaluateStatusCode(final ResponseEntity<T> response) { + if(response.getStatusCode().equals(HttpStatus.NOT_FOUND)){ + throw new RestApiException(response.getStatusCode(), "requested entity could not be found"); + } + return response.getBody(); + } + private HttpEntity<String> getHttpEntity(final HttpHeaders headers) { return getHttpEntity(null, headers); } diff --git a/core/src/main/java/dev/fitko/fitconnect/core/util/EventLogUtil.java b/core/src/main/java/dev/fitko/fitconnect/core/util/EventLogUtil.java index 875133cc2011a6d9e3d9ea584d7617d97dff888f..aad6d08a97b22e227f72124789621887b73ae69f 100644 --- a/core/src/main/java/dev/fitko/fitconnect/core/util/EventLogUtil.java +++ b/core/src/main/java/dev/fitko/fitconnect/core/util/EventLogUtil.java @@ -4,19 +4,31 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import dev.fitko.fitconnect.api.domain.model.event.*; +import dev.fitko.fitconnect.api.config.ApplicationConfig; +import dev.fitko.fitconnect.api.domain.model.event.Event; +import dev.fitko.fitconnect.api.domain.model.event.EventClaimFields; +import dev.fitko.fitconnect.api.domain.model.event.EventIssuer; +import dev.fitko.fitconnect.api.domain.model.event.EventLog; +import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry; +import dev.fitko.fitconnect.api.domain.model.event.TypeAndUUID; import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.exceptions.EventCreationException; import dev.fitko.fitconnect.api.exceptions.EventLogException; import java.text.ParseException; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.function.Predicate; import java.util.stream.Collectors; import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; -import static dev.fitko.fitconnect.api.domain.model.event.EventIssuer.*; +import static dev.fitko.fitconnect.api.domain.model.event.EventIssuer.DESTINATION; +import static dev.fitko.fitconnect.api.domain.model.event.EventIssuer.SUBMISSION_SERVICE; +import static dev.fitko.fitconnect.api.domain.model.event.EventIssuer.UNDEFINED; public final class EventLogUtil { @@ -79,8 +91,8 @@ public final class EventLogUtil { final String eventName = eventEntry.getKey(); final Map<String, Object> eventPayload = eventEntry.getValue(); final List<Problem> problems = new ArrayList<>(); - if (eventPayload.containsKey("problems")) { - problems.addAll((List<Problem>) eventPayload.get("problems")); + if (eventPayload.containsKey(EventClaimFields.PROBLEMS)) { + problems.addAll((List<Problem>) eventPayload.get(EventClaimFields.PROBLEMS)); } return EventLogEntry.builder() .event(Event.fromSchemaUri(eventName)) @@ -88,7 +100,7 @@ public final class EventLogUtil { .issueTime(claimsSet.getIssueTime()) .eventId(getEventId(claimsSet)) .caseId(getTransactionClaim(claimsSet)) - .submissionId(getSubmissionId(claimsSet)) + .submissionId(getSubmissionIdFromJWT(claimsSet)) .problems(problems) .build(); } @@ -107,10 +119,14 @@ public final class EventLogUtil { } } - private static UUID getSubmissionId(final JWTClaimsSet claimsSet) { + public static UUID getSubmissionIdFromJWT(final JWTClaimsSet claimsSet) { return TypeAndUUID.fromString(claimsSet.getSubject()).getUuid(); } + public static UUID getSubmissionIdFromJWT(final SignedJWT signedJWT) { + return TypeAndUUID.fromString(getClaimsSet(signedJWT).getSubject()).getUuid(); + } + private static UUID getEventId(final JWTClaimsSet claimsSet) { return UUID.fromString(claimsSet.getJWTID()); } @@ -139,18 +155,38 @@ public final class EventLogUtil { } } + public static AuthenticationTags getAuthenticationTags(final SignedJWT jwt) { + return getAuthenticationTags(getClaimsSet(jwt)); + } + public static AuthenticationTags getAuthenticationTags(final JWTClaimsSet claims) { try { final Map<String, Object> eventsClaim = (Map) claims.getClaim(EventClaimFields.CLAIM_EVENTS); final Map.Entry<String, Map> eventEntry = (Map.Entry) eventsClaim.entrySet().iterator().next(); final Map<String, Object> events = eventEntry.getValue(); - final String authTags = MAPPER.writeValueAsString(events.get("authenticationTags")); + final String authTags = MAPPER.writeValueAsString(events.get(EventClaimFields.AUTHENTICATION_TAGS)); return MAPPER.readValue(authTags, AuthenticationTags.class); } catch (final JsonProcessingException e) { throw new EventCreationException(e.getMessage(), e); } } + public static String getAuthenticationTagFromEncryptedData(final String encryptedData) { + try { + final String[] parts = encryptedData.split(ApplicationConfig.AUTH_TAG_SPLIT_TOKEN); + return parts[parts.length - 1]; + } catch (final Exception e) { + return null; + } + } + + public static List<SignedJWT> getJwtsFromEvents(final Event event, final Submission submission, final EventLog eventLog) { + return getJWTSFromEvents(eventLog.getEventLogs()).stream() + .filter(jwt -> getSubmissionIdFromJWT(jwt).equals(submission.getSubmissionId())) + .filter(jwt -> getEventFromJWT(jwt).equals(event)) + .collect(Collectors.toList()); + } + public static Predicate<SignedJWT> eventEqualsSubmissionId(final UUID submissionId) { return jwt -> { try { 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 3f666e9629185d1bbbb2ba016e3361190f7a245c..9bfa9535a8cb903e6fa3faa52973a5e02a7ed2de 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 @@ -1,5 +1,6 @@ package dev.fitko.fitconnect.core.validation; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JacksonException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -11,8 +12,26 @@ import com.nimbusds.jose.jwk.KeyOperation; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.util.Base64; import dev.fitko.fitconnect.api.config.ApplicationConfig; +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.EventClaimFields; +import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; +import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.AttachmentHashMismatch; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.IncorrectAttachmentAuthenticationTag; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataHashMismatch; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataJsonSyntaxViolation; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataXmlSyntaxViolation; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.IncorrectDataAuthenticationTag; +import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.*; import dev.fitko.fitconnect.api.domain.model.metadata.Metadata; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.ApiAttachment; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation; +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.replychannel.ReplyChannel; +import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.validation.ValidationResult; import dev.fitko.fitconnect.api.exceptions.DataIntegrityException; import dev.fitko.fitconnect.api.exceptions.RootCertificateException; @@ -47,20 +66,20 @@ import java.text.SimpleDateFormat; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; +import java.util.stream.Stream; +import static dev.fitko.fitconnect.core.util.EventLogUtil.getAuthenticationTagFromEncryptedData; import static dev.fitko.fitconnect.jwkvalidator.JWKValidator.withX5CValidation; import static dev.fitko.fitconnect.jwkvalidator.JWKValidator.withoutX5CValidation; +import static java.util.Objects.isNull; 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 ObjectMapper MAPPER = new ObjectMapper().setDateFormat(new SimpleDateFormat("yyyy-MM-dd")).setSerializationInclusion(JsonInclude.Include.NON_NULL); 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"; @@ -78,19 +97,9 @@ public class DefaultValidationService implements ValidationService { } @Override - public ValidationResult validateEncryptionPublicKey(final RSAKey publicKey) { + public ValidationResult validatePublicKey(final RSAKey publicKey, final KeyOperation keyOperation) { try { - validateKey(publicKey, KeyOperation.WRAP_KEY); - } catch (final Exception e) { - return ValidationResult.error(e); - } - return ValidationResult.ok(); - } - - @Override - public ValidationResult validateSignaturePublicKey(final RSAKey signatureKey) { - try { - validateKey(signatureKey, KeyOperation.VERIFY); + validateKey(publicKey, keyOperation); } catch (final Exception e) { return ValidationResult.error(e); } @@ -116,7 +125,9 @@ public class DefaultValidationService implements ValidationService { public ValidationResult validateMetadataSchema(final Metadata metadata) { if (metadata.getSchema() != null && !metadata.getSchema().isEmpty() && !metadata.getSchema().matches(VALID_SCHEMA_URL_EXPRESSION)) { - return ValidationResult.error(new ValidationException("The provided metadata schema is not supported.")); + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#required-schema-reference + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#metadatenschema + return ValidationResult.problem(new UnsupportedMetadataSchema(metadata.getSchema())); } try { @@ -124,7 +135,8 @@ public class DefaultValidationService implements ValidationService { final JsonNode inputNode = MAPPER.readTree(metadataJson); return validate2020JsonSchema(schemaProvider.loadMetadataSchema(config.getMetadataSchemaWriteVersion()), inputNode); } catch (final JsonProcessingException e) { - return ValidationResult.error(e); + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#schema-pr%C3%BCfung + return ValidationResult.withErrorAndProblem(e, new MetadataSchemaViolation()); } } @@ -146,7 +158,7 @@ public class DefaultValidationService implements ValidationService { final byte[] originalHash = messageDigestService.fromHexString(originalHexHash); final boolean hashesAreNotEqual = !messageDigestService.verify(originalHash, data); if (hashesAreNotEqual) { - return ValidationResult.error(new DataIntegrityException("Hash sum of transmitted data does not equal the hash of the sender.")); + return ValidationResult.error(new DataIntegrityException("Metadata contains invalid hash value")); } return ValidationResult.ok(); } catch (final IllegalArgumentException | NullPointerException e) { @@ -154,6 +166,7 @@ public class DefaultValidationService implements ValidationService { } } + @Override public ValidationResult validateJsonFormat(final String json) { try { @@ -193,6 +206,148 @@ public class DefaultValidationService implements ValidationService { return ValidationResult.ok(); } + @Override + public ValidationResult validateData(final byte[] decryptedData, final Submission submission, final Metadata metadata, final AuthenticationTags authenticationTags) { + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#authentication-tag-pr%C3%BCfen-1 + final var dataAuthTag = getAuthenticationTagFromEncryptedData(submission.getEncryptedData()); + if (!authenticationTags.getData().equals(dataAuthTag)) { + return ValidationResult.problem(new IncorrectDataAuthenticationTag()); + } + + final Data data = metadata.getContentStructure().getData(); + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#submission-data-hash + final String hashFromSender = data.getHash().getContent(); + final ValidationResult hashValidation = validateHashIntegrity(hashFromSender, decryptedData); + if (hashValidation.hasError()) { + return ValidationResult.problem(new DataHashMismatch()); + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#syntax-validierung-1 + if (data.getSubmissionSchema().getMimeType().equals(MimeType.APPLICATION_JSON)) { + final ValidationResult jsonValidation = validateJsonFormat(new String(decryptedData, StandardCharsets.UTF_8)); + if (jsonValidation.hasError()) { + return ValidationResult.problem(new DataJsonSyntaxViolation()); + } + } + + if (data.getSubmissionSchema().getMimeType().equals(MimeType.APPLICATION_XML)) { + final ValidationResult xmlValidation = validateXmlFormat(new String(decryptedData, StandardCharsets.UTF_8)); + if (xmlValidation.hasError()) { + return ValidationResult.problem(new DataXmlSyntaxViolation()); + } + } + return ValidationResult.ok(); + } + + @Override + public ValidationResult validateAttachments(final List<AttachmentForValidation> attachmentsForValidation, final AuthenticationTags authenticationTags) { + + final Map<UUID, String> eventAuthTags = authenticationTags.getAttachments(); + final List<Problem> validationProblems = new ArrayList<>(); + + for (final AttachmentForValidation attachment : attachmentsForValidation) { + + final UUID attachmentId = attachment.getAttachmentId(); + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#authentication-tag-pr%C3%BCfen-2 + final String authTagFromEvent = eventAuthTags.get(attachmentId); + final String authTagFromAttachment = getAuthenticationTagFromEncryptedData(attachment.getEncryptedData()); + if (!authTagFromEvent.equals(authTagFromAttachment)) { + validationProblems.add(new IncorrectAttachmentAuthenticationTag(attachmentId)); + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#attachment-hash + final ValidationResult validationResult = validateHashIntegrity(attachment.getMetadataHash(), attachment.getDecryptedData()); + if (validationResult.hasError()) { + validationProblems.add(new AttachmentHashMismatch(attachmentId)); + } + } + + return validationProblems.isEmpty() ? ValidationResult.ok() : ValidationResult.problems(validationProblems); + } + + @Override + public ValidationResult validateMetadata(final Metadata metadata, final Submission submission, final Destination destination, final AuthenticationTags eventAuthenticationTags) { + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#authentication-tag-pr%C3%BCfen + final var submissionMetadataAuthTag = getAuthenticationTagFromEncryptedData(submission.getEncryptedMetadata()); + if (!eventAuthenticationTags.getMetadata().equals(submissionMetadataAuthTag)) { + return ValidationResult.problem(new IncorrectMetadataAuthenticationTag()); + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#syntax-validierung + if (invalidJsonSyntax(metadata)) { + return ValidationResult.problem(new MetadataJsonSyntaxViolation()); + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#fachdatensatz + if (metadata.getContentStructure().getData() == null) { + return ValidationResult.problem(new MissingData()); + } + + final ValidationResult validationResult = validateMetadataSchema(metadata); + if (validationResult.hasProblems()) { + return validationResult; + } + + if (metadata.getPublicServiceType() != null) { + final String serviceIdentifier = metadata.getPublicServiceType().getIdentifier(); + if (!serviceIdentifier.equals(submission.getServiceType().getIdentifier())) { + return ValidationResult.problem(new ServiceMismatch()); + } + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#verwaltungsleistung-abgleichen-ii + final var destinationService = destination.getServices() + .stream() + .map(DestinationService::getIdentifier) + .filter(identifier -> identifier.equals(submission.getServiceType().getIdentifier())) + .findFirst(); + + if (destinationService.isEmpty()) { + return ValidationResult.problem(new UnsupportedService()); + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#fachdatenschema + if (destination.getServices().isEmpty()) { + return ValidationResult.problem(new UnsupportedDataSchema()); + } + final URI submissionDataSchemaUri = metadata.getContentStructure().getData().getSubmissionSchema().getSchemaUri(); + final boolean matchingSchemas = matchingDestinationAndSubmissionSchema(destination, submissionDataSchemaUri); + if (!matchingSchemas) { + return ValidationResult.problem(new UnsupportedDataSchema()); + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#liste-der-anlagen-abgleichen + final ValidationResult attachmentValidation = checkAttachmentCount(submission, metadata, eventAuthenticationTags); + if (attachmentValidation.hasProblems()) { + return attachmentValidation; + } + + // https://docs.fitko.de/fit-connect/docs/receiving/verification/#r%C3%BCckkanal + final ReplyChannel submissionReplyChannel = metadata.getReplyChannel(); + final ReplyChannel destinationReplyChannel = destination.getReplyChannels(); + + if (submissionReplyChannel != null) { + final List<Class<?>> submissionReplyChannelClass = getNonNullReplyChannelTypes(submissionReplyChannel); + final List<Class<?>> destinationReplyChannelClass = getNonNullReplyChannelTypes(destinationReplyChannel); + if (!new HashSet<>(destinationReplyChannelClass).containsAll(submissionReplyChannelClass)) { + return ValidationResult.problem(new UnsupportedReplyChannel()); + } + } + + return ValidationResult.ok(); + } + + private static boolean matchingDestinationAndSubmissionSchema(final Destination destination, final URI submissionDataSchemaUri) { + return destination.getServices().stream() + .flatMap(service -> service.getSubmissionSchemas().stream()) + .map(SubmissionSchema::getSchemaUri) + .anyMatch(submissionDataSchemaUri::equals); + } + private ValidationResult validate2020JsonSchema(final String schema, final JsonNode inputNode) { return returnValidationResult(SCHEMA_FACTORY_DRAFT_2020.getSchema(schema).validate(inputNode)); } @@ -209,7 +364,7 @@ public class DefaultValidationService implements ValidationService { if (errors.isEmpty()) { return ValidationResult.ok(); } - return ValidationResult.error(new ValidationException(errorsToSingleString(errors))); + return ValidationResult.withErrorAndProblem(new ValidationException(errorsToSingleString(errors)), new MetadataSchemaViolation()); } private JWKValidator addLogLevel(final JWKValidatorBuilder.JWKValidatorX5CErrorHandling validator) { @@ -229,12 +384,12 @@ public class DefaultValidationService implements ValidationService { void validateCertChain(final RSAKey publicKey, final KeyOperation purpose) throws JWKValidationException { final List<X509Certificate> x509CertChain = publicKey.getParsedX509CertChain(); - if(x509CertChain == null){ - throw new IllegalStateException("public key with id '"+publicKey.getKeyID()+"' does not contain a certificate chain"); + if (x509CertChain == null) { + throw new IllegalStateException("public key with id '" + publicKey.getKeyID() + "' does not contain a certificate chain"); } final List<String> trustedRootCertificates = this.loadTrustedRootCertificates(); LOGGER.info("Validation public key with XC5 certificate chain checks"); - if (isProxySet()) { + if (config.isProxySet()) { validateWithX509AndProxy(trustedRootCertificates, publicKey, purpose); } else { validateWithX509AndWithoutProxy(trustedRootCertificates, publicKey, purpose); @@ -289,4 +444,43 @@ public class DefaultValidationService implements ValidationService { return saxParserFactory.newSAXParser().getXMLReader(); } + private static List<Class<?>> getNonNullReplyChannelTypes(final ReplyChannel destinationReplyChannel) { + return Stream.of(destinationReplyChannel.getEMail(), + destinationReplyChannel.getDeMail(), + destinationReplyChannel.getFink(), + destinationReplyChannel.getElster()) + .filter(Objects::nonNull) + .map(Object::getClass) + .collect(Collectors.toList()); + } + + private boolean invalidJsonSyntax(final Metadata metadata) { + try { + return metadata == null || validateJsonFormat(MAPPER.writeValueAsString(metadata)).hasError(); + } catch (final JsonProcessingException | NullPointerException e) { + return true; + } + } + + private ValidationResult checkAttachmentCount(final Submission submission, final Metadata metadata, final AuthenticationTags authenticationTags) { + + final List<UUID> submissionAttachmentIds = submission.getAttachments(); + final List<ApiAttachment> metadataAttachments = metadata.getContentStructure().getAttachments(); + final Map<UUID, String> attachmentAuthTags = authenticationTags.getAttachments(); + + if ((isNull(submissionAttachmentIds) || submissionAttachmentIds.isEmpty()) && (isNull(attachmentAuthTags) || attachmentAuthTags.isEmpty())) { + return ValidationResult.ok(); + } + + if (metadataAttachments.size() != submissionAttachmentIds.size() + || attachmentAuthTags.size() != submissionAttachmentIds.size() + || !attachmentAuthTags.keySet().containsAll(submissionAttachmentIds) + || !submissionAttachmentIds.stream().allMatch(id -> metadataAttachments.stream().anyMatch(m -> m.getAttachmentId().equals(id))) + || !submissionAttachmentIds.stream().allMatch(attachmentAuthTags::containsKey)) { + return ValidationResult.problem(new AttachmentsMismatch()); + } + + return ValidationResult.ok(); + } + } diff --git a/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSenderTest.java b/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSenderTest.java index bc2c94a5011bde267e1792813ab0e818febe77a1..88ee81c44b3cddc395b3325f31adae0bcacb26ce 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSenderTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSenderTest.java @@ -83,25 +83,6 @@ public class SubmissionSenderTest { ); } - @Test - void validatePublicKey() throws JOSEException { - - //Given - final RSAKey publicKey = new RSAKeyGenerator(2048) - .keyUse(KeyUse.ENCRYPTION) - .keyID(UUID.randomUUID().toString()) - .generate() - .toPublicJWK(); - - when(validationServiceMock.validateEncryptionPublicKey(any())).thenReturn(ValidationResult.ok()); - - // When - final ValidationResult validationResult = underTest.validatePublicKey(publicKey); - - // Then - assertTrue(validationResult.isValid()); - } - @Test void encryptBytes() throws JOSEException { diff --git a/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSubscriberTest.java b/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSubscriberTest.java index 4f1cec09d6cf6df36b3d29b3f441d9bd0db33e6d..08813b25f7a59a1d805dccdaf31f56828c0c8506 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSubscriberTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/SubmissionSubscriberTest.java @@ -7,8 +7,14 @@ import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; import com.nimbusds.jwt.SignedJWT; import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry; import dev.fitko.fitconnect.api.domain.model.event.EventPayload; +import dev.fitko.fitconnect.api.domain.model.event.authtags.AuthenticationTags; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataHashMismatch; +import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.MetadataSchemaViolation; import dev.fitko.fitconnect.api.domain.model.event.problems.submission.AttachmentsMismatch; +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.attachment.ApiAttachment; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation; import dev.fitko.fitconnect.api.domain.model.submission.Submission; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionsForPickup; @@ -33,6 +39,7 @@ import java.util.UUID; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -40,6 +47,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -167,10 +175,10 @@ class SubmissionSubscriberTest { void validateMetadata() { // Given - when(validationServiceMock.validateMetadataSchema(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validateMetadata(any(), any(), any(), any())).thenReturn(ValidationResult.ok()); // When - final ValidationResult validationResult = underTest.validateMetadata(new Metadata()); + final ValidationResult validationResult = underTest.validateMetadata(new Metadata(), new Submission(), new AuthenticationTags()); // Then assertTrue(validationResult.isValid()); @@ -180,14 +188,28 @@ class SubmissionSubscriberTest { void invalidMetadata() { // Given - when(validationServiceMock.validateMetadataSchema(any())).thenReturn(ValidationResult.error(new Exception("Failed parsing metadata"))); + when(validationServiceMock.validateMetadata(any(), any(), any(), any())).thenReturn(ValidationResult.problem(new MetadataSchemaViolation())); // When - final ValidationResult validationResult = underTest.validateMetadata(new Metadata()); + final ValidationResult validationResult = underTest.validateMetadata(new Metadata(), new Submission(), new AuthenticationTags()); // Then - assertTrue(validationResult.hasError()); - assertThat(validationResult.getError().getMessage(), containsString("Failed parsing metadata")); + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(MetadataSchemaViolation.class)); + } + + @Test + void invalidData() { + + // Given + when(validationServiceMock.validateData(any(), any(), any(), any())).thenReturn(ValidationResult.problem(new DataHashMismatch())); + + // When + final ValidationResult validationResult = underTest.validateData("foo".getBytes(), new Submission(), new Metadata(), new AuthenticationTags()); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(DataHashMismatch.class)); } @Test @@ -300,6 +322,29 @@ class SubmissionSubscriberTest { assertThrows(EventLogException.class, () -> underTest.getEventLog(destinationId, caseId)); } + @Test + void testAttachmentValidationFailed() { + + // Given + final Hash hash = new Hash(); + hash.setContent("hashValue"); + + final ApiAttachment attachmentMetadata = new ApiAttachment(); + attachmentMetadata.setAttachmentId(UUID.randomUUID()); + attachmentMetadata.setHash(hash); + + final AttachmentForValidation attachmentForValidation = new AttachmentForValidation(attachmentMetadata, null, null); + + when(validationServiceMock.validateAttachments(anyList(), any())).thenReturn(ValidationResult.problem(new AttachmentsMismatch())); + + // When + final ValidationResult validationResult = underTest.validateAttachments(List.of(attachmentForValidation), new AuthenticationTags()); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(AttachmentsMismatch.class)); + } + private String getResourceAsString(final String filename) throws IOException { return new String(SubmissionSenderTest.class.getResourceAsStream(filename).readAllBytes()); 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 4a3231e07256d377aea1428d6eac17c7a84f2bb9..5ffbed66bd10d45e100021a38cca55da4d91ff51 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 @@ -75,7 +75,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -113,7 +113,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId, authenticationTags); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -142,7 +142,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId, null); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -178,7 +178,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId, authTags); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -216,7 +216,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -254,7 +254,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.error(new SchemaNotFoundException("Schema not found"))); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -289,7 +289,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -330,7 +330,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -371,7 +371,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -410,7 +410,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -450,7 +450,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getSubmissionServicePublicKey(any())).thenReturn(rsaKey); // When @@ -488,7 +488,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -526,7 +526,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -563,7 +563,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -602,7 +602,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(rsaKey); // When @@ -639,7 +639,7 @@ class EventLogVerifierTest { final ValidationContext ctx = new ValidationContext(destinationId, caseId); when(validationServiceMock.validateSetEventSchema(any())).thenReturn(ValidationResult.ok()); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); when(keyServiceMock.getPublicSignatureKey(any(), any())).thenReturn(null); // When diff --git a/core/src/test/java/dev/fitko/fitconnect/core/events/ValidationContextTest.java b/core/src/test/java/dev/fitko/fitconnect/core/events/ValidationContextTest.java index 8d8e02bd07fe80da53d8cb7c9c2976015ec2e292..4ff9591ebf96533d8efba98dd3081ca1f7929a6c 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/events/ValidationContextTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/events/ValidationContextTest.java @@ -23,7 +23,7 @@ class ValidationContextTest { final ValidationContext underTest = new ValidationContext(UUID.randomUUID(), UUID.randomUUID()); // When - underTest.addResult(1+2 == 3, "1+2 does not equal 3"); + underTest.addErrorIfTestFailed(1+2 == 3, "1+2 does not equal 3"); final List<ValidationResult> results = underTest.getValidationResults(); // Then @@ -37,7 +37,7 @@ class ValidationContextTest { final ValidationContext underTest = new ValidationContext(UUID.randomUUID(), UUID.randomUUID()); // When - underTest.addResult(1+2 == 2, "1+2 does not equal 3"); + underTest.addErrorIfTestFailed(1+2 == 2, "1+2 does not equal 3"); final List<ValidationResult> results = underTest.getValidationResults(); // Then @@ -71,7 +71,7 @@ class ValidationContextTest { final List<ValidationResult> results = underTest.getValidationResults(); // Then - assertTrue(results.size() == 1); + assertEquals(1, results.size()); assertTrue(results.get(0).hasError()); assertThat(results.get(0).getError().getMessage(), is("Test failed")); } @@ -87,7 +87,7 @@ class ValidationContextTest { final List<ValidationResult> results = underTest.getValidationResults(); // Then - assertTrue(results.size() == 1); + assertEquals(1, results.size()); assertTrue(results.get(0).hasError()); assertThat(results.get(0).getError().getMessage(), is("Test failed")); } 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 54b90334da0c9a89fc1ae3323418afa358daa032..82e637c367d39bdc0adf42987fadb1878dcc3429 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 @@ -91,7 +91,7 @@ class PublicKeyServiceTest extends RestEndpointBase { when(authServiceMock.getCurrentToken()).thenReturn(authToken); when(submissionServiceMock.getDestination(any())).thenReturn(destination); - when(validationServiceMock.validateEncryptionPublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); // When final RSAKey rsaKey = underTest.getPublicEncryptionKey(destination.getDestinationId()); @@ -134,7 +134,7 @@ class PublicKeyServiceTest extends RestEndpointBase { when(authServiceMock.getCurrentToken()).thenReturn(authToken); when(submissionServiceMock.getDestination(any())).thenReturn(destination); - when(validationServiceMock.validateEncryptionPublicKey(any())).thenReturn(ValidationResult.error(new Exception("Public key is insecure !"))); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.error(new Exception("Public key is insecure !"))); // When final InvalidKeyException exception = assertThrows(InvalidKeyException.class, () -> keyService.getPublicEncryptionKey(destination.getDestinationId())); @@ -165,7 +165,7 @@ class PublicKeyServiceTest extends RestEndpointBase { when(authServiceMock.getCurrentToken()).thenReturn(authToken); when(submissionServiceMock.getDestination(any())).thenReturn(destination); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); // When final RSAKey retrievedSignatureKey = underTest.getPublicSignatureKey(destination.getDestinationId(), "123"); @@ -195,7 +195,7 @@ class PublicKeyServiceTest extends RestEndpointBase { .withStatus(200))); when(authServiceMock.getCurrentToken()).thenReturn(authToken); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); // When final RSAKey retrievedSignatureKey = underTest.getSubmissionServicePublicKey("123"); @@ -225,7 +225,7 @@ class PublicKeyServiceTest extends RestEndpointBase { .withStatus(200))); when(authServiceMock.getCurrentToken()).thenReturn(authToken); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); // When final RSAKey retrievedSignatureKey = underTest.getPortalPublicKey("123"); @@ -255,7 +255,7 @@ class PublicKeyServiceTest extends RestEndpointBase { .withStatus(200))); when(authServiceMock.getCurrentToken()).thenReturn(authToken); - when(validationServiceMock.validateSignaturePublicKey(any())).thenReturn(ValidationResult.ok()); + when(validationServiceMock.validatePublicKey(any(), any())).thenReturn(ValidationResult.ok()); // When final RSAKey retrievedSignatureKey = underTest.getWellKnownKeysForSubmissionUrl("http://localhost:" + wireMockServer.port() + "/custom/path", "123"); diff --git a/core/src/test/java/dev/fitko/fitconnect/core/util/EventLogUtilTest.java b/core/src/test/java/dev/fitko/fitconnect/core/util/EventLogUtilTest.java index b2b4367b2b31f5b389432bfb5efadd69c6a7ac86..4ba6454ce2cc4e9b9fa5527b4f2fcb2037743d3a 100644 --- a/core/src/test/java/dev/fitko/fitconnect/core/util/EventLogUtilTest.java +++ b/core/src/test/java/dev/fitko/fitconnect/core/util/EventLogUtilTest.java @@ -5,6 +5,7 @@ import dev.fitko.fitconnect.api.domain.model.event.Event; import dev.fitko.fitconnect.api.domain.model.event.EventIssuer; import dev.fitko.fitconnect.api.domain.model.event.EventLog; import dev.fitko.fitconnect.api.domain.model.event.EventLogEntry; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataEncryptionIssue; import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog; import dev.fitko.fitconnect.api.exceptions.EventLogException; import org.junit.jupiter.api.Test; 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 72cadba47b33ae64b7030be930c97a90fc42fb33..877380dce434e0c33e6e842661d05835f1ef5dde 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 @@ -12,16 +12,30 @@ 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.config.SchemaConfig; -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.SignatureType; +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.event.problems.Problem; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.AttachmentHashMismatch; +import dev.fitko.fitconnect.api.domain.model.event.problems.attachment.IncorrectAttachmentAuthenticationTag; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataHashMismatch; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataJsonSyntaxViolation; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataXmlSyntaxViolation; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.IncorrectDataAuthenticationTag; +import dev.fitko.fitconnect.api.domain.model.event.problems.metadata.*; +import dev.fitko.fitconnect.api.domain.model.metadata.*; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.ApiAttachment; +import dev.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentForValidation; 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.replychannel.Email; +import dev.fitko.fitconnect.api.domain.model.replychannel.Fink; +import dev.fitko.fitconnect.api.domain.model.replychannel.ReplyChannel; +import dev.fitko.fitconnect.api.domain.model.submission.ServiceType; +import dev.fitko.fitconnect.api.domain.model.submission.Submission; 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; import dev.fitko.fitconnect.api.services.schema.SchemaProvider; import dev.fitko.fitconnect.core.crypto.HashService; @@ -39,10 +53,11 @@ import java.net.URI; import java.text.ParseException; import java.time.ZonedDateTime; import java.util.*; +import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.*; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; @@ -56,11 +71,13 @@ class DefaultValidationServiceTest { @BeforeEach void setUp() { final var config = getApplicationConfig(true); - hashService = new HashService(); + final List<String> setSchemas = SchemaConfig.getSetSchemaFilePaths("/set-schema"); final List<String> metadataSchemas = SchemaConfig.getMetadataSchemaFileNames("/metadata-schema"); final List<String> destinationSchemas = SchemaConfig.getDestinationSchemaPaths("/destination-schema"); final SchemaResources schemaResources = new SchemaResources(setSchemas, metadataSchemas, destinationSchemas); + + hashService = new HashService(); schemaProvider = new SchemaResourceProvider(schemaResources); underTest = new DefaultValidationService(config, hashService, schemaProvider, FileUtil.loadContentOfFilesInDirectory("trusted-test-root-certificates")); @@ -77,7 +94,7 @@ class DefaultValidationServiceTest { .generate(); // When - final ValidationResult validationResult = underTest.validateEncryptionPublicKey(rsaKey); + final ValidationResult validationResult = underTest.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY); // Then assertTrue(validationResult.isValid()); @@ -94,7 +111,7 @@ class DefaultValidationServiceTest { .generate(); // When - final ValidationResult validationResult = underTest.validateEncryptionPublicKey(rsaKey); + final ValidationResult validationResult = underTest.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY); // Then assertTrue(validationResult.hasError()); @@ -122,7 +139,7 @@ class DefaultValidationServiceTest { final RSAKey rsaKey = getRsaKeyWithCertChain(); // When - underTest.validateEncryptionPublicKey(rsaKey); + underTest.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY); // Then logs.assertContains("Using proxy HTTP @ https://localhost:8080 for key validation"); @@ -143,7 +160,7 @@ class DefaultValidationServiceTest { .generate(); // When - final ValidationResult validationResult = underTest.validateEncryptionPublicKey(rsaKey); + final ValidationResult validationResult = underTest.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY); // Then assertTrue(validationResult.isValid()); @@ -164,7 +181,7 @@ class DefaultValidationServiceTest { .generate(); // When - final ValidationResult validationResult = underTest.validateEncryptionPublicKey(rsaKey); + final ValidationResult validationResult = underTest.validatePublicKey(rsaKey, KeyOperation.WRAP_KEY); // Then assertTrue(validationResult.hasError()); @@ -173,7 +190,7 @@ class DefaultValidationServiceTest { @Test - void validateMetadata() { + void testValidMetadataSchema() { // Given final var submissionSchema = new SubmissionSchema(); @@ -203,6 +220,474 @@ class DefaultValidationServiceTest { assertTrue(validationResult.isValid()); } + @Test + void testValidMetadata() { + + // Given + 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 var hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash("someBogusContent".getBytes()))); + hash.setSignatureType(SignatureType.SHA_512); + + final var data = new Data(); + data.setSubmissionSchema(submissionSchema); + data.setHash(hash); + + final var attachmentOne = new ApiAttachment(); + attachmentOne.setAttachmentId(UUID.randomUUID()); + + final var attachmentTwo = new ApiAttachment(); + attachmentTwo.setAttachmentId(UUID.randomUUID()); + + final var contentStructure = new ContentStructure(); + contentStructure.setData(data); + contentStructure.setAttachments(List.of(attachmentOne, attachmentTwo)); + + final PublicServiceType publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final ReplyChannel replyChannel = new ReplyChannel(); + replyChannel.setEMail(new Email("test@mail.net", false, null)); + + final var metadata = new Metadata(); + metadata.setSchema(SchemaConfig.METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + metadata.setReplyChannel(replyChannel); + + final ServiceType serviceType = new ServiceType(); + serviceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + submission.setServiceType(serviceType); + submission.setAttachments(contentStructure.getAttachments().stream().map(ApiAttachment::getAttachmentId).collect(Collectors.toList())); + + final DestinationService destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + destinationService.setSubmissionSchemas(Set.of(submissionSchema)); + + final var destination = new Destination(); + destination.setServices(Set.of(destinationService)); + destination.setReplyChannels(replyChannel); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + authenticationTags.setAttachments(Map.of(attachmentOne.getAttachmentId(), "authTag", attachmentTwo.getAttachmentId(), "authTag")); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.isValid()); + } + + @Test + void testIncorrectMetadataAuthenticationTag() { + + // Given + final var metadata = new Metadata(); + final var destination = new Destination(); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("wrongMetadataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), is(new IncorrectMetadataAuthenticationTag())); + } + + + @Test + void testUnsupportedMetadataSchema() { + + // Given + final String unsupportedSchema = "https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json/metadata_schema_3.2.1.json"; + + final Data data = new Data(); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setData(data); + + final var metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + metadata.setSchema(unsupportedSchema); + final var destination = new Destination(); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), is(new UnsupportedMetadataSchema(unsupportedSchema))); + } + + @Test + void testMetadataSyntaxViolation() { + + // Given + final var destination = new Destination(); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateMetadata(null, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(MetadataJsonSyntaxViolation.class)); + } + + @Test + void testMissingData() { + + // Given + final var destination = new Destination(); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + final var metadata = new Metadata(); + metadata.setContentStructure(new ContentStructure()); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(MissingData.class)); + } + + @Test + void testMetadataSchemaViolation() { + + // Given + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setData(new Data()); + + final var metadata = new Metadata(); + metadata.setSchema(SchemaConfig.METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + + final var destination = new Destination(); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), is(new MetadataSchemaViolation())); + } + + @Test + void testServiceMismatch() { + + // Given + final var hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash("someBogusContent".getBytes()))); + hash.setSignatureType(SignatureType.SHA_512); + + final SubmissionSchema submissionSchema = new SubmissionSchema(); + submissionSchema.setSchemaUri(URI.create("https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json")); + submissionSchema.setMimeType(MimeType.APPLICATION_JSON); + + final var data = new Data(); + data.setSubmissionSchema(submissionSchema); + data.setHash(hash); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setData(data); + contentStructure.setAttachments(Collections.emptyList()); + + final PublicServiceType publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier("urn:de:fim:leika:leistung:123456789"); + + final var metadata = new Metadata(); + metadata.setSchema(SchemaConfig.METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + + final var destination = new Destination(); + + final ServiceType submissionServiceType = new ServiceType(); + submissionServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + submission.setServiceType(submissionServiceType); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(ServiceMismatch.class)); + } + + @Test + void testUnsupportedService() { + + // Given + final SubmissionSchema submissionSchema = new SubmissionSchema(); + submissionSchema.setSchemaUri(URI.create("https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json")); + submissionSchema.setMimeType(MimeType.APPLICATION_JSON); + + final Hash hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash("someBogusContent".getBytes()))); + hash.setSignatureType(SignatureType.SHA_512); + + final Data data = new Data(); + data.setSubmissionSchema(submissionSchema); + data.setHash(hash); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setAttachments(Collections.emptyList()); + contentStructure.setData(data); + + final PublicServiceType publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var metadata = new Metadata(); + metadata.setSchema(SchemaConfig.METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + + final DestinationService destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:9999999999999"); + + final var destination = new Destination(); + destination.setServices(Set.of(destinationService)); + + final ServiceType submissionServiceType = new ServiceType(); + submissionServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + submission.setServiceType(submissionServiceType); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(UnsupportedService.class)); + } + + @Test + void testUnsupportedDataSchema() { + + // Given + final SubmissionSchema submissionSchema = new SubmissionSchema(); + submissionSchema.setSchemaUri(URI.create("https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json")); + submissionSchema.setMimeType(MimeType.APPLICATION_JSON); + + final Hash hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash("someBogusContent".getBytes()))); + hash.setSignatureType(SignatureType.SHA_512); + + final Data data = new Data(); + data.setSubmissionSchema(submissionSchema); + data.setHash(hash); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setAttachments(Collections.emptyList()); + contentStructure.setData(data); + + final PublicServiceType publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var metadata = new Metadata(); + metadata.setSchema(SchemaConfig.METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + + final SubmissionSchema destinationSubmissionSchema = new SubmissionSchema(); + destinationSubmissionSchema.setSchemaUri(URI.create("https://schema.fitko.de/fit-connect/metadata/3.0.0/metadata.schema.json")); + destinationSubmissionSchema.setMimeType(MimeType.APPLICATION_JSON); + + final DestinationService destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + destinationService.setSubmissionSchemas(Set.of(destinationSubmissionSchema)); + + final var destination = new Destination(); + destination.setServices(Set.of(destinationService)); + + final ServiceType submissionServiceType = new ServiceType(); + submissionServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + submission.setServiceType(submissionServiceType); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(UnsupportedDataSchema.class)); + } + + @Test + void testAttachmentsMismatch() { + + // Given + final SubmissionSchema schema = new SubmissionSchema(); + schema.setSchemaUri(URI.create("https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json")); + schema.setMimeType(MimeType.APPLICATION_JSON); + + final Hash hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash("someBogusContent".getBytes()))); + hash.setSignatureType(SignatureType.SHA_512); + + final Data data = new Data(); + data.setSubmissionSchema(schema); + data.setHash(hash); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setData(data); + + final PublicServiceType publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var metadata = new Metadata(); + metadata.setSchema(SchemaConfig.METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + + final DestinationService destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + destinationService.setSubmissionSchemas(Set.of(schema)); + + final var destination = new Destination(); + destination.setServices(Set.of(destinationService)); + + final ServiceType submissionServiceType = new ServiceType(); + submissionServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + submission.setServiceType(submissionServiceType); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + // attachmentId that were announced + submission.setAttachments(List.of(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())); + + // attachment in metadata that doesn't match + final ApiAttachment attachment = new ApiAttachment(); + attachment.setAttachmentId(UUID.randomUUID()); + contentStructure.setAttachments(List.of(attachment)); + + // authTag from submit event + authenticationTags.setAttachments(Map.of(attachment.getAttachmentId(), "attachmentAuthTag")); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(AttachmentsMismatch.class)); + } + + @Test + void testUnsupportedReplyChannel() { + + // Given + final SubmissionSchema schema = new SubmissionSchema(); + schema.setSchemaUri(URI.create("https://schema.fitko.de/fit-connect/metadata/1.0.0/metadata.schema.json")); + schema.setMimeType(MimeType.APPLICATION_JSON); + + final Hash hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash("someBogusContent".getBytes()))); + hash.setSignatureType(SignatureType.SHA_512); + + final Data data = new Data(); + data.setSubmissionSchema(schema); + data.setHash(hash); + + final ContentStructure contentStructure = new ContentStructure(); + contentStructure.setAttachments(Collections.emptyList()); + contentStructure.setData(data); + + final PublicServiceType publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var metadata = new Metadata(); + metadata.setSchema(SchemaConfig.METADATA_V_1_0_0.toString()); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + + final DestinationService destinationService = new DestinationService(); + destinationService.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + destinationService.setSubmissionSchemas(Set.of(schema)); + + final var destination = new Destination(); + destination.setServices(Set.of(destinationService)); + + final ServiceType submissionServiceType = new ServiceType(); + submissionServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); + + final var submission = new Submission(); + submission.setEncryptedMetadata("header.encryption_key.init_vector.ciphertext.metadataAuthTag"); + submission.setServiceType(submissionServiceType); + + final var authenticationTags = new AuthenticationTags(); + authenticationTags.setMetadata("metadataAuthTag"); + + final ReplyChannel submissionReplyChannel = new ReplyChannel(); + submissionReplyChannel.setEMail(new Email("test@mail.org", false, null)); + metadata.setReplyChannel(submissionReplyChannel); + + final ReplyChannel destinationReplyChannel = new ReplyChannel(); + destinationReplyChannel.setFink(new Fink("finkPostBoxRef", "finkHost")); + destination.setReplyChannels(destinationReplyChannel); + + // When + final ValidationResult validationResult = underTest.validateMetadata(metadata, submission, destination, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(UnsupportedReplyChannel.class)); + } + @Test void validateMetadataWithoutSchemaAttributeButValidSchema() { @@ -223,9 +708,8 @@ class DefaultValidationServiceTest { final ValidationResult validationResult = underTest.validateMetadataSchema(metadata); - assertTrue(validationResult.hasError()); - assertThat(validationResult.getError().getClass(), equalTo(ValidationException.class)); - assertThat(validationResult.getError().getMessage(), containsString("The provided metadata schema is not supported.")); + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems(), contains(new UnsupportedMetadataSchema(metadata.getSchema()))); } @Test @@ -287,6 +771,276 @@ class DefaultValidationServiceTest { assertThat(validationResult.getError().getMessage(), containsString("$.contentStructure.data.hash.content: does not match the regex pattern ^[a-f0-9]{128}$")); } + @Test + void testIncorrectAttachmentAuthenticationTag() { + + // Given + final var firstAttachmentId = UUID.randomUUID(); + final var secondAttachmentId = UUID.randomUUID(); + + final var firstDecryptedData = "first test attachment".getBytes(); + final var firstEncryptedData = "part1.part2.part3.part4.authTag1"; + final var firstOriginalHash = hashService.toHexString(hashService.createHash(firstDecryptedData)); + + final byte[] secondDecryptedData = "second test attachment".getBytes(); + final var secondEncryptedData = "part1.part2.part3.part4.authTag2"; + final var secondOriginalHash = hashService.toHexString(hashService.createHash(secondDecryptedData)); + + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setAttachments(Map.of(firstAttachmentId, "notMatchingAuthTag1", secondAttachmentId, "notMatchingAuthTag2")); + + final Hash firstHash = new Hash(); + firstHash.setContent(firstOriginalHash); + + final ApiAttachment firstAttachmentMetadata = new ApiAttachment(); + firstAttachmentMetadata.setAttachmentId(firstAttachmentId); + firstAttachmentMetadata.setHash(firstHash); + + final Hash secondHash = new Hash(); + secondHash.setContent(secondOriginalHash); + + final ApiAttachment secondAttachmentMetadata = new ApiAttachment(); + secondAttachmentMetadata.setAttachmentId(secondAttachmentId); + secondAttachmentMetadata.setHash(secondHash); + + final AttachmentForValidation firstAttachment = new AttachmentForValidation(firstAttachmentMetadata, firstEncryptedData, firstDecryptedData); + final AttachmentForValidation secondAttachment = new AttachmentForValidation(secondAttachmentMetadata, secondEncryptedData, secondDecryptedData); + + // When + final ValidationResult validationResult = underTest.validateAttachments(List.of(firstAttachment, secondAttachment), authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems(), hasSize(2)); + assertThat(validationResult.getProblems().stream().map(Problem::getClass).collect(Collectors.toList()), hasItems(IncorrectAttachmentAuthenticationTag.class)); + assertThat(validationResult.getProblems().get(0).getInstance(), containsString("attachment:" + firstAttachmentId)); + assertThat(validationResult.getProblems().get(1).getInstance(), containsString("attachment:" + secondAttachmentId)); + } + + @Test + void testAttachmentHashMismatch() { + + // Given + final var firstAttachmentId = UUID.randomUUID(); + final var secondAttachmentId = UUID.randomUUID(); + + final var firstDecryptedData = "first test attachment".getBytes(); + final var firstEncryptedData = "part1.part2.part3.part4.authTag1"; + final var firstOriginalHash = hashService.toHexString(hashService.createHash(firstDecryptedData)); + + final byte[] secondDecryptedData = "second test attachment".getBytes(); + final var secondEncryptedData = "part1.part2.part3.part4.authTag2"; + final var secondOriginalHash = hashService.toHexString(hashService.createHash(secondDecryptedData)); + + final AuthenticationTags authenticationTags = new AuthenticationTags(); + authenticationTags.setAttachments(Map.of(firstAttachmentId, "authTag1", secondAttachmentId, "authTag2")); + + final Hash firstHash = new Hash(); + firstHash.setContent(firstOriginalHash); + + final ApiAttachment firstAttachmentMetadata = new ApiAttachment(); + firstAttachmentMetadata.setAttachmentId(firstAttachmentId); + firstAttachmentMetadata.setHash(firstHash); + + final Hash secondHash = new Hash(); + secondHash.setContent(secondOriginalHash); + + final ApiAttachment secondAttachmentMetadata = new ApiAttachment(); + secondAttachmentMetadata.setAttachmentId(secondAttachmentId); + secondAttachmentMetadata.setHash(secondHash); + + final AttachmentForValidation firstAttachment = new AttachmentForValidation(firstAttachmentMetadata, firstEncryptedData, "modified attachment 1 content".getBytes()); + final AttachmentForValidation secondAttachment = new AttachmentForValidation(secondAttachmentMetadata, secondEncryptedData, "modified attachment 2 content".getBytes()); + + // When + final ValidationResult validationResult = underTest.validateAttachments(List.of(firstAttachment, secondAttachment), authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems(), hasSize(2)); + assertThat(validationResult.getProblems().stream().map(Problem::getClass).collect(Collectors.toList()), hasItems(AttachmentHashMismatch.class)); + assertThat(validationResult.getProblems().get(0).getInstance(), containsString("attachment:" + firstAttachmentId)); + assertThat(validationResult.getProblems().get(1).getInstance(), containsString("attachment:" + secondAttachmentId)); + } + + @Test + void testValidData() { + + // Given + final var decryptedData = "{ \"fachdaten\" : \"test\"}".getBytes(); + + final var submission = new Submission(); + submission.setEncryptedData("part1.part2.part3.part4.dataAuthTag"); + + 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 var hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash(decryptedData))); + hash.setSignatureType(SignatureType.SHA_512); + + final var data = new Data(); + data.setHash(hash); + data.setSubmissionSchema(submissionSchema); + + final var contentStructure = new ContentStructure(); + contentStructure.setData(data); + contentStructure.setAttachments(Collections.emptyList()); + + final var metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + + final var authenticationTags =new AuthenticationTags(); + authenticationTags.setData("dataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateData(decryptedData, submission, metadata, authenticationTags); + + // Then + assertTrue(validationResult.isValid()); + } + + @Test + void testIncorrectDataAuthenticationTag() { + + // Given + final var decryptedData = "{ \"fachdaten\" : \"test\"}".getBytes(); + + final var submission = new Submission(); + submission.setEncryptedData("part1.part2.part3.part4.wrongDataAuthTag"); + + final var hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash(decryptedData))); + hash.setSignatureType(SignatureType.SHA_512); + + final var data = new Data(); + data.setHash(hash); + + final var contentStructure = new ContentStructure(); + contentStructure.setData(data); + + final var metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + + final var authenticationTags =new AuthenticationTags(); + authenticationTags.setData("dataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateData(decryptedData, submission, metadata, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(IncorrectDataAuthenticationTag.class)); + } + + @Test + void testDataHashMismatch() { + + // Given + final var decryptedData = "{ \"fachdaten\" : \"test\"}".getBytes(); + + final var submission = new Submission(); + submission.setEncryptedData("part1.part2.part3.part4.dataAuthTag"); + + final var hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash("manipulated data".getBytes()))); + hash.setSignatureType(SignatureType.SHA_512); + + final var data = new Data(); + data.setHash(hash); + + final var contentStructure = new ContentStructure(); + contentStructure.setData(data); + + final var metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + + final var authenticationTags =new AuthenticationTags(); + authenticationTags.setData("dataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateData(decryptedData, submission, metadata, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(DataHashMismatch.class)); + } + + @Test + void testDataJsonSyntaxViolation() { + + // Given + final var decryptedData = "{ invalid_json }".getBytes(); + + final var submission = new Submission(); + submission.setEncryptedData("part1.part2.part3.part4.dataAuthTag"); + + final var hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash(decryptedData))); + hash.setSignatureType(SignatureType.SHA_512); + + final var submissionSchema = new SubmissionSchema(); + submissionSchema.setMimeType(MimeType.APPLICATION_JSON); + + final var data = new Data(); + data.setHash(hash); + data.setSubmissionSchema(submissionSchema); + + final var contentStructure = new ContentStructure(); + contentStructure.setData(data); + + final var metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + + final var authenticationTags =new AuthenticationTags(); + authenticationTags.setData("dataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateData(decryptedData, submission, metadata, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(DataJsonSyntaxViolation.class)); + } + + @Test + void testDataXmlSyntaxViolation() { + + // Given + final var decryptedData = "<xmehl>invalid xml</xml> }".getBytes(); + + final var submission = new Submission(); + submission.setEncryptedData("part1.part2.part3.part4.dataAuthTag"); + + final var hash = new Hash(); + hash.setContent(hashService.toHexString(hashService.createHash(decryptedData))); + hash.setSignatureType(SignatureType.SHA_512); + + final var submissionSchema = new SubmissionSchema(); + submissionSchema.setMimeType(MimeType.APPLICATION_XML); + + final var data = new Data(); + data.setHash(hash); + data.setSubmissionSchema(submissionSchema); + + final var contentStructure = new ContentStructure(); + contentStructure.setData(data); + + final var metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + + final var authenticationTags =new AuthenticationTags(); + authenticationTags.setData("dataAuthTag"); + + // When + final ValidationResult validationResult = underTest.validateData(decryptedData, submission, metadata, authenticationTags); + + // Then + assertTrue(validationResult.hasProblems()); + assertThat(validationResult.getProblems().get(0), instanceOf(DataXmlSyntaxViolation.class)); + } + @Test void validateMatchingHash() { @@ -311,7 +1065,7 @@ class DefaultValidationServiceTest { // Then assertTrue(validationResult.hasError()); - assertThat(validationResult.getError().getMessage(), containsString("transmitted data does not equal the hash of the sender")); + assertThat(validationResult.getError().getMessage(), containsString("Metadata contains invalid hash value")); } @Test diff --git a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/EventLogIT.java b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/EventLogIT.java index 303b0fb9f712892245618ae69dd90722393f0061..299736cb0d46579ca952fbb98f7f37f59f9c4fc4 100644 --- a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/EventLogIT.java +++ b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/EventLogIT.java @@ -5,15 +5,20 @@ 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.SubmissionState; import dev.fitko.fitconnect.api.domain.model.event.SubmissionStatus; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.DataEncryptionIssue; +import dev.fitko.fitconnect.api.domain.model.event.problems.data.IncorrectDataAuthenticationTag; import dev.fitko.fitconnect.api.domain.model.event.problems.submission.InvalidEventLog; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; +import dev.fitko.fitconnect.api.exceptions.SubmissionRequestException; import dev.fitko.fitconnect.client.SenderClient; +import dev.fitko.fitconnect.client.SubscriberClient; import dev.fitko.fitconnect.client.factory.ClientFactory; import dev.fitko.fitconnect.client.sender.model.Attachment; import dev.fitko.fitconnect.client.sender.model.SendableSubmission; import dev.fitko.fitconnect.integrationtests.condition.EnableIfEnvironmentVariablesAreSet; import org.awaitility.Awaitility; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.nio.file.Path; @@ -25,20 +30,30 @@ import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.collection.IsEmptyCollection.empty; 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; @EnableIfEnvironmentVariablesAreSet public class EventLogIT extends IntegrationTestBase { + @BeforeEach + public void cleanup(){ + cleanupTestSubmissions(); + } + @Test void testRejectEvent() { // Given final ApplicationConfig config = getConfigWithCredentialsFromEnvironment("TESTING", true); + final SenderClient senderClient = ClientFactory.getSenderClient(config); + final var submission = SendableSubmission.Builder() .setDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) .setServiceType("urn:de:fim:leika:leistung:99400048079000", "Test Service") @@ -46,9 +61,9 @@ public class EventLogIT extends IntegrationTestBase { .addAttachment(Attachment.fromPath(Path.of("src/test/resources/attachment.txt"), "plain/text")) .build(); - final var sentSubmission = ClientFactory.getSenderClient(config).send(submission); + final var sentSubmission = senderClient.send(submission); - Assertions.assertNotNull(sentSubmission); + assertNotNull(sentSubmission); // When final var subscriberClient = ClientFactory.getSubscriberClient(config); @@ -56,11 +71,16 @@ public class EventLogIT extends IntegrationTestBase { final var sentSubmissionId = sentSubmission.getSubmissionId(); // reject and remove - subscriberClient.requestSubmission(sentSubmissionId).rejectSubmission(List.of(new InvalidEventLog())); + subscriberClient.requestSubmission(sentSubmissionId).rejectSubmission(List.of(new DataEncryptionIssue())); + + // check event log if reject event was sent + final SubmissionStatus status = senderClient.getStatusForSubmission(sentSubmission); + assertThat(status.getStatus(), is(SubmissionState.REJECTED)); // second attempt to receive and reject the submission should return an empty result // since the submission is gone after being rejected - Assertions.assertNull(subscriberClient.requestSubmission(sentSubmissionId)); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> subscriberClient.requestSubmission(sentSubmissionId)); + assertThat(exception.getMessage(), containsString("Submission not found")); } @Test @@ -69,6 +89,9 @@ public class EventLogIT extends IntegrationTestBase { // Given final ApplicationConfig config = getConfigWithCredentialsFromEnvironment("TESTING", true); + final SenderClient senderClient = ClientFactory.getSenderClient(config); + final var subscriberClient = ClientFactory.getSubscriberClient(config); + final var submission = SendableSubmission.Builder() .setDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) .setServiceType("urn:de:fim:leika:leistung:99400048079000", "Test Service") @@ -76,21 +99,65 @@ public class EventLogIT extends IntegrationTestBase { .addAttachment(Attachment.fromPath(Path.of("src/test/resources/attachment.txt"), "plain/text")) .build(); - final var sentSubmission = ClientFactory.getSenderClient(config).send(submission); + final var sentSubmission = senderClient.send(submission); Assertions.assertNotNull(sentSubmission); // When - final var subscriberClient = ClientFactory.getSubscriberClient(config); final var sentSubmissionId = sentSubmission.getSubmissionId(); // accept and remove subscriberClient.requestSubmission(sentSubmissionId).acceptSubmission(); + // check event log if accept event was sent + final SubmissionStatus status = senderClient.getStatusForSubmission(sentSubmission); + assertThat(status.getStatus(), is(SubmissionState.ACCEPTED)); + // second attempt to receive the submission should return an empty result // since the submission is gone after being accepted - Assertions.assertNull(subscriberClient.requestSubmission(sentSubmissionId)); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> subscriberClient.requestSubmission(sentSubmissionId)); + assertThat(exception.getMessage(), containsString("Submission not found")); + } + + @Test + void testAcceptEventWithProblem() { + + // Given + final ApplicationConfig config = getConfigWithCredentialsFromEnvironment("TESTING", true); + + final var submission = SendableSubmission.Builder() + .setDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) + .setServiceType("urn:de:fim:leika:leistung:99400048079000", "Test Service") + .setJsonData("{ \"data\": \"Beispiel Fachdaten\" }") + .addAttachment(Attachment.fromPath(Path.of("src/test/resources/attachment.txt"), "plain/text")) + .build(); + + final var sentSubmission = ClientFactory.getSenderClient(config).send(submission); + + assertNotNull(sentSubmission); + + // When + final var subscriberClient = ClientFactory.getSubscriberClient(config); + + final var sentSubmissionId = sentSubmission.getSubmissionId(); + + // accept and remove + subscriberClient.requestSubmission(sentSubmissionId).acceptSubmission(new IncorrectDataAuthenticationTag()); + + // check event log if accept event was sent and contains a problem + final Optional<EventLogEntry> acceptEvent = subscriberClient + .getEventLog(sentSubmission.getCaseId(), sentSubmission.getDestinationId()) + .stream().filter(e -> e.getEvent().equals(Event.ACCEPT)) + .findFirst(); + + assertTrue(acceptEvent.isPresent()); + assertThat(acceptEvent.get().getProblems(), is(not(empty()))); + + // second attempt to receive the submission should return an empty result + // since the submission is gone after being accepted + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> subscriberClient.requestSubmission(sentSubmissionId)); + assertThat(exception.getMessage(), containsString("Submission not found")); } @Test @@ -127,8 +194,10 @@ public class EventLogIT extends IntegrationTestBase { // Then assertThat(senderClient.getStatusForSubmission(sentSubmission).getStatus(), is(SubmissionState.REJECTED)); + // second attempt to receive and reject the submission should return an empty result since the submission is gone after being rejected - assertNull(subscriberClient.requestSubmission(sentSubmission.getSubmissionId())); + final SubmissionRequestException exception = assertThrows(SubmissionRequestException.class, () -> subscriberClient.requestSubmission(sentSubmission.getSubmissionId())); + assertThat(exception.getMessage(), containsString("Submission not found")); } @Test @@ -136,6 +205,7 @@ public class EventLogIT extends IntegrationTestBase { // Given final ApplicationConfig config = getConfigWithCredentialsFromEnvironment("TESTING", true); + final SenderClient senderClient = ClientFactory.getSenderClient(config); final var submission = SendableSubmission.Builder() .setDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) @@ -143,7 +213,7 @@ public class EventLogIT extends IntegrationTestBase { .setJsonData("{ \"data\": \"Beispiel Fachdaten\" }") .build(); - final var sentSubmission = ClientFactory.getSenderClient(config).send(submission); + final var sentSubmission = senderClient.send(submission); Assertions.assertNotNull(sentSubmission); @@ -153,7 +223,7 @@ public class EventLogIT extends IntegrationTestBase { final UUID destinationId = sentSubmission.getDestinationId(); final UUID caseId = sentSubmission.getCaseId(); - final List<EventLogEntry> senderEventLog = ClientFactory.getSenderClient(config).getEventLog(caseId, destinationId); + final List<EventLogEntry> senderEventLog = senderClient.getEventLog(caseId, destinationId); final List<Event> senderEvents = senderEventLog.stream().map(EventLogEntry::getEvent).collect(Collectors.toList()); // Then @@ -167,6 +237,9 @@ public class EventLogIT extends IntegrationTestBase { // Given final ApplicationConfig config = getConfigWithCredentialsFromEnvironment("TESTING", true); + final SenderClient senderClient = ClientFactory.getSenderClient(config); + final SubscriberClient subscriberClient = ClientFactory.getSubscriberClient(config); + final var submission = SendableSubmission.Builder() .setDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) .setServiceType("urn:de:fim:leika:leistung:99400048079000", "Test Service") @@ -174,22 +247,23 @@ public class EventLogIT extends IntegrationTestBase { .addAttachment(Attachment.fromPath(Path.of("src/test/resources/attachment.txt"), "plain/text")) .build(); - final var sentSubmission = ClientFactory.getSenderClient(config).send(submission); + final var sentSubmission = senderClient.send(submission); Assertions.assertNotNull(sentSubmission); - final var receivedSubmission = ClientFactory.getSubscriberClient(config) + final var receivedSubmission = subscriberClient .requestSubmission(sentSubmission.getSubmissionId()); Assertions.assertNotNull(receivedSubmission); + // When Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> { final UUID destinationId = receivedSubmission.getDestinationId(); final UUID caseId = receivedSubmission.getCaseId(); - final List<EventLogEntry> subscriberEventLog = ClientFactory.getSubscriberClient(config).getEventLog(caseId, destinationId); + final List<EventLogEntry> subscriberEventLog = subscriberClient.getEventLog(caseId, destinationId); final List<Event> subscriberEvents = subscriberEventLog.stream().map(EventLogEntry::getEvent).collect(Collectors.toList()); // Then diff --git a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/IntegrationTestBase.java b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/IntegrationTestBase.java index 0a2388502585d03a642ee8a38ba8a4c3a3a5f0d3..3e890f51f841946c7a79af91cd053041daa444bd 100644 --- a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/IntegrationTestBase.java +++ b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/IntegrationTestBase.java @@ -8,7 +8,6 @@ import dev.fitko.fitconnect.api.config.SubscriberConfig; import dev.fitko.fitconnect.api.domain.model.event.problems.Problem; import dev.fitko.fitconnect.client.SubscriberClient; import dev.fitko.fitconnect.client.factory.ClientFactory; -import org.junit.jupiter.api.BeforeAll; import java.util.List; import java.util.Map; @@ -23,8 +22,7 @@ public class IntegrationTestBase { private static final String selfServicePortalUrl = "https://portal.auth-testing.fit-connect.fitko.dev"; private static final String submissionBaseUrl = "https://submission-api-testing.fit-connect.fitko.dev"; - @BeforeAll - static void cleanupSubmissionByRejecting() { + public static void cleanupTestSubmissions() { final UUID destinationId = UUID.fromString(System.getenv("TEST_DESTINATION_ID")); final Problem problem = new Problem(SCHEMA_URL + "technical-error", "cleanup", "submission-cleanup", "other"); @@ -32,7 +30,7 @@ public class IntegrationTestBase { subscriberClient.getAvailableSubmissionsForDestination(destinationId).forEach((s -> { try { - subscriberClient.requestSubmission(s.getSubmissionId()).rejectSubmission(List.of(problem)); + subscriberClient.rejectSubmission(s, List.of(problem)); } catch (final Exception e) { //continue } diff --git a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SenderClientIT.java b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SenderClientIT.java index 6d37a93e509824eef54547d6099aff91ca8b3543..0be4adb0af94dbd8949bee321fead0bc93014419 100644 --- a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SenderClientIT.java +++ b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SenderClientIT.java @@ -19,7 +19,6 @@ import dev.fitko.fitconnect.api.services.crypto.CryptoService; import dev.fitko.fitconnect.client.SenderClient; import dev.fitko.fitconnect.client.factory.ClientFactory; import dev.fitko.fitconnect.client.sender.model.Attachment; -import dev.fitko.fitconnect.client.sender.model.AttachmentPayload; import dev.fitko.fitconnect.client.sender.model.EncryptedAttachment; import dev.fitko.fitconnect.client.sender.model.SendableEncryptedSubmission; import dev.fitko.fitconnect.client.sender.model.SendableSubmission; @@ -31,6 +30,7 @@ import org.apache.tika.mime.MimeTypes; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.File; @@ -51,6 +51,11 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; @EnableIfEnvironmentVariablesAreSet public class SenderClientIT extends IntegrationTestBase { + @BeforeEach + public void cleanup(){ + cleanupTestSubmissions(); + } + @Test void testSendAndConfirmCycle() { @@ -64,7 +69,7 @@ public class SenderClientIT extends IntegrationTestBase { .addAttachment(Attachment.fromPath(Path.of("src/test/resources/attachment.txt"), "text/plain")) .addAttachment(Attachment.fromByteArray("attachment data".getBytes(), "text/plain")) .addAttachment(Attachment.fromString("attachment data", "text/plain")) - .setReplyChannel(ReplyChannel.fromEmail("test@mail.org")) + .setReplyChannel(ReplyChannel.fromDeMail("test@mail.org")) .build(); final var sentSubmission = ClientFactory.getSenderClient(config).send(submission); @@ -82,7 +87,7 @@ public class SenderClientIT extends IntegrationTestBase { assertThat(receivedSubmission.getDataSchemaUri(), is(URI.create("https://schema.fitko.de/fim/s00000000009_1.0.0.schema.json"))); assertThat(receivedSubmission.getDataMimeType(), is("application/json")); assertThat(receivedSubmission.getAttachments(), hasSize(3)); - assertThat(receivedSubmission.getMetadata().getReplyChannel(), is(ReplyChannel.fromEmail("test@mail.org"))); + assertThat(receivedSubmission.getMetadata().getReplyChannel(), is(ReplyChannel.fromDeMail("test@mail.org"))); assertThat(new String(receivedSubmission.getAttachments().get(0).getDataAsBytes()), is("Test attachment")); } @@ -120,25 +125,18 @@ public class SenderClientIT extends IntegrationTestBase { data.setHash(dataHash); data.setSubmissionSchema(submissionSchema); - final var attachmentPayload = AttachmentPayload.builder() - .encryptedData(encryptedAttachment) - .mimeType(MimeTypes.PLAIN_TEXT) - .attachmentId(UUID.randomUUID()) - .hashedData(cryptoService.hashBytes(attachmentData.getBytes(StandardCharsets.UTF_8))) - .build(); - final var publicServiceType = new PublicServiceType(); publicServiceType.setName("Test Service"); publicServiceType.setIdentifier("urn:de:fim:leika:leistung:99400048079000"); final var attachment = new ApiAttachment(); - attachment.setAttachmentId(attachmentPayload.getAttachmentId()); + attachment.setAttachmentId(UUID.randomUUID()); attachment.setPurpose(Purpose.ATTACHMENT); - attachment.setFilename(attachmentPayload.getFileName()); - attachment.setMimeType(attachmentPayload.getMimeType()); + attachment.setFilename(attachmentFile.getName()); + attachment.setMimeType(MimeTypes.PLAIN_TEXT); final var attachmentHash = new Hash(); - attachmentHash.setContent(attachmentPayload.getHashedData()); + attachmentHash.setContent(cryptoService.hashBytes(attachmentData.getBytes(StandardCharsets.UTF_8))); attachmentHash.setSignatureType(SignatureType.SHA_512); attachment.setHash(attachmentHash); @@ -159,7 +157,7 @@ public class SenderClientIT extends IntegrationTestBase { .setServiceType("urn:de:fim:leika:leistung:99400048079000", "Test Service") .setEncryptedMetadata(encryptedMetadata) .setEncryptedData(encryptedData) - .addEncryptedAttachment(new EncryptedAttachment(attachmentPayload.getAttachmentId(), attachmentPayload.getEncryptedData())) + .addEncryptedAttachment(new EncryptedAttachment(attachment.getAttachmentId(), encryptedAttachment)) .build(); final var sentSubmission = ClientFactory.getSenderClient(config).send(submission); diff --git a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SubscriberClientIT.java b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SubscriberClientIT.java index b4eb69b484a93e2a9fb36855bef3c0e0060be87e..2c8dd6bda70482d4c11fb259538759da9b200387 100644 --- a/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SubscriberClientIT.java +++ b/integration-tests/src/test/java/dev/fitko/fitconnect/integrationtests/SubscriberClientIT.java @@ -3,21 +3,34 @@ package dev.fitko.fitconnect.integrationtests; import dev.fitko.fitconnect.api.config.ApplicationConfig; import dev.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import dev.fitko.fitconnect.client.factory.ClientFactory; +import dev.fitko.fitconnect.client.sender.model.Attachment; import dev.fitko.fitconnect.client.sender.model.SendableSubmission; +import dev.fitko.fitconnect.client.subscriber.ReceivedSubmission; import dev.fitko.fitconnect.integrationtests.condition.EnableIfEnvironmentVariablesAreSet; -import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; + @EnableIfEnvironmentVariablesAreSet public class SubscriberClientIT extends IntegrationTestBase { + @BeforeEach + public void cleanup(){ + cleanupTestSubmissions(); + } + @Test void testListSubmissionsForDestination() { @@ -46,8 +59,8 @@ public class SubscriberClientIT extends IntegrationTestBase { final var sentSubmissionOne = senderClient.send(submissionOne); final var sentSubmissionTwo = senderClient.send(submissionTwo); - Assertions.assertNotNull(sentSubmissionOne); - Assertions.assertNotNull(sentSubmissionTwo); + assertNotNull(sentSubmissionOne); + assertNotNull(sentSubmissionTwo); // When final Set<SubmissionForPickup> submissions = subscriberClient.getAvailableSubmissionsForDestination(destinationId); @@ -57,10 +70,44 @@ public class SubscriberClientIT extends IntegrationTestBase { final List<UUID> submissionIds = submissions.stream().map(SubmissionForPickup::getSubmissionId).collect(Collectors.toList()); - MatcherAssert.assertThat(submissionIds, Matchers.hasItems(sentSubmissionOne.getSubmissionId(), sentSubmissionTwo.getSubmissionId())); + assertThat(submissionIds, Matchers.hasItems(sentSubmissionOne.getSubmissionId(), sentSubmissionTwo.getSubmissionId())); // remove by confirming subscriberClient.requestSubmission(sentSubmissionOne.getSubmissionId()).acceptSubmission(); subscriberClient.requestSubmission(sentSubmissionTwo.getSubmissionId()).acceptSubmission(); } + + @Test + void testReceiveSingleSubmission() { + + // Given + final ApplicationConfig config = getConfigWithCredentialsFromEnvironment("TESTING", true); + + final var senderClient = ClientFactory.getSenderClient(config); + final var subscriberClient = ClientFactory.getSubscriberClient(config); + + final UUID destinationId = UUID.fromString(System.getenv("TEST_DESTINATION_ID")); + final String leikaKey = "urn:de:fim:leika:leistung:99400048079000"; + final String serviceName = "Test Service"; + + final var submission = SendableSubmission.Builder() + .setDestination(destinationId) + .setServiceType(leikaKey, serviceName) + .setJsonData("{ \"data\": \"Beispiel Fachdaten 1\" }") + .addAttachment(Attachment.fromString("foo", "plain/text")) + .build(); + + final var sentSubmission = senderClient.send(submission); + + assertNotNull(sentSubmission); + + // When + final ReceivedSubmission receivedSubmission = subscriberClient.requestSubmission(sentSubmission.getSubmissionId()); + + // Then + assertNotNull(receivedSubmission); + assertThat(receivedSubmission.getAttachments(), hasSize(1)); + assertThat(receivedSubmission.getAttachments().get(0).getDataAString(StandardCharsets.UTF_8), is("foo")); + assertThat(receivedSubmission.getDataAsString(), is("{ \"data\": \"Beispiel Fachdaten 1\" }")); + } } diff --git a/pom.xml b/pom.xml index 639b1f1aef0bec26d459bd7b16fb39d7d0366e4e..d32b872daaa04a464d10d5d17de1404e29b99ce0 100644 --- a/pom.xml +++ b/pom.xml @@ -66,10 +66,10 @@ <jackson-annotations.version>2.14.2</jackson-annotations.version> <lombock.version>1.18.26</lombock.version> <logback.version>1.4.6</logback.version> - <slf4j.version>2.0.6</slf4j.version> + <slf4j.version>2.0.7</slf4j.version> <jcommander.version>1.82</jcommander.version> <apache-tika.version>2.7.0</apache-tika.version> - <spring-web.version>5.3.25</spring-web.version> + <spring-web.version>5.3.26</spring-web.version> <snakeyaml.version>2.0</snakeyaml.version> <open-csv.version>5.7.1</open-csv.version> <json-schema-validator.version>1.0.78</json-schema-validator.version>