From 9bf810eeedbd44b385b67af814aa25fa9d22a462 Mon Sep 17 00:00:00 2001 From: Martin Vogel <martin.vogel@sinc.de> Date: Fri, 9 Sep 2022 10:05:33 +0000 Subject: [PATCH] =?UTF-8?q?Senden=20von=20bereits=20verschl=C3=BCsselten?= =?UTF-8?q?=20Antr=C3=A4gen=20inkl.=20Abruf=20von=20PK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fitko/fitconnect/client/SenderClient.java | 533 ++++++++---------- .../fitconnect/client/SubscriberClient.java | 13 +- .../client/cmd/CommandLineClient.java | 35 +- .../client/factory/ClientFactory.java | 4 +- .../client/model/AttachmentPayload.java | 20 + .../fitconnect/client/model/DataPayload.java | 19 + .../client/model/ReceivedSubmission.java | 18 + .../client/model/ServiceTypePayload.java | 12 + .../client/model/SubmissionPayload.java | 26 + .../SendEncryptedSubmissionStrategy.java | 92 +++ .../strategies/SendNewSubmissionStrategy.java | 234 ++++++++ .../client/strategies/SubmitStrategy.java | 30 + .../client/util/SubmissionUtil.java | 139 +++++ .../client/ClientIntegrationTest.java | 115 +++- .../fitconnect/client/SenderClientTest.java | 126 +++-- .../client/cmd/CommandLineClientTest.java | 13 +- client/src/test/resources/test-config.yml | 8 +- .../core/auth/DefaultOAuthService.java | 2 +- 18 files changed, 1043 insertions(+), 396 deletions(-) create mode 100644 client/src/main/java/de/fitko/fitconnect/client/model/AttachmentPayload.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/model/DataPayload.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/model/ReceivedSubmission.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/model/ServiceTypePayload.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/model/SubmissionPayload.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/strategies/SendEncryptedSubmissionStrategy.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/strategies/SendNewSubmissionStrategy.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/strategies/SubmitStrategy.java create mode 100644 client/src/main/java/de/fitko/fitconnect/client/util/SubmissionUtil.java diff --git a/client/src/main/java/de/fitko/fitconnect/client/SenderClient.java b/client/src/main/java/de/fitko/fitconnect/client/SenderClient.java index ea6225595..872f0f081 100644 --- a/client/src/main/java/de/fitko/fitconnect/client/SenderClient.java +++ b/client/src/main/java/de/fitko/fitconnect/client/SenderClient.java @@ -3,37 +3,30 @@ package de.fitko.fitconnect.client; import com.nimbusds.jose.jwk.RSAKey; import de.fitko.fitconnect.api.config.Environment; import de.fitko.fitconnect.api.domain.model.destination.Destination; -import de.fitko.fitconnect.api.domain.model.destination.DestinationService; -import de.fitko.fitconnect.api.domain.model.metadata.ContentStructure; -import de.fitko.fitconnect.api.domain.model.metadata.Metadata; -import de.fitko.fitconnect.api.domain.model.metadata.PublicServiceType; -import de.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; -import de.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose; -import de.fitko.fitconnect.api.domain.model.metadata.attachment.signature.AttachmentSignatureType; -import de.fitko.fitconnect.api.domain.model.metadata.attachment.signature.Hash__1; -import de.fitko.fitconnect.api.domain.model.metadata.data.*; -import de.fitko.fitconnect.api.domain.model.submission.CreateSubmission; +import de.fitko.fitconnect.api.domain.model.metadata.data.MimeType; import de.fitko.fitconnect.api.domain.model.submission.ServiceType; -import de.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import de.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; import de.fitko.fitconnect.api.domain.validation.ValidationResult; -import de.fitko.fitconnect.api.exceptions.*; +import de.fitko.fitconnect.api.exceptions.KeyNotRetrievedException; +import de.fitko.fitconnect.api.exceptions.RestApiException; import de.fitko.fitconnect.api.services.Sender; -import lombok.Builder; -import lombok.Getter; -import lombok.With; -import org.apache.tika.Tika; +import de.fitko.fitconnect.client.model.AttachmentPayload; +import de.fitko.fitconnect.client.model.DataPayload; +import de.fitko.fitconnect.client.model.ServiceTypePayload; +import de.fitko.fitconnect.client.model.SubmissionPayload; +import de.fitko.fitconnect.client.strategies.SendEncryptedSubmissionStrategy; +import de.fitko.fitconnect.client.strategies.SendNewSubmissionStrategy; +import de.fitko.fitconnect.client.strategies.SubmitStrategy; +import de.fitko.fitconnect.client.util.SubmissionUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.net.URLConnection; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.*; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; /** @@ -46,10 +39,35 @@ public final class SenderClient { private SenderClient() { } - public static WithDestination build(final Sender sender, final String metadataSchema, final Environment environment) { + public static Start build(final Sender sender, final String metadataSchema, final Environment environment) { return new ClientBuilder(sender, metadataSchema, environment); } + public interface Start { + + /** + * Creates new submission request. + * + * @return next step to add (optional) attachments + */ + WithAttachments newSubmission(); + + /** + * Creates new submission request wit already encrypted data. + * + * @return next step to add (optional) attachments + */ + WithEncryptedAttachments newSubmissionWithEncryptedData(); + + /** + * Retrieve the public encryption key + * + * @param destinationId unique identifier of a {@link Destination} + * @return optional string containing the public JWK + */ + Optional<String> getPublicKey(UUID destinationId); + } + public interface WithDestination { /** @@ -71,6 +89,7 @@ public final class SenderClient { */ WithData withAttachments(List<File> attachments); + /** * Sends the submission with an attachments * @@ -79,21 +98,59 @@ public final class SenderClient { */ WithData withAttachment(File attachment); + /** * XML data as string. * * @param data json string * @return next step to submit the data */ - Submit withJsonData(String data); + WithDestination withJsonData(String data); /** - * JSON data as string. + * XML data as string. * * @param data xml string * @return next step to submit the data */ - Submit withXmlData(String data); + WithDestination withXmlData(String data); + } + + public interface WithEncryptedAttachments { + + /** + * Sends the submission with a list of already encrypted attachments + * + * @param encryptedAttachments map of encrypted attachments on attachment UUIDs + * + * @return the next step where additional encrypted data can be added to the submission + */ + WithEncryptedData withEncryptedAttachments(Map<UUID, String> encryptedAttachments); + + /** + * Sends the submission with an attachments + * + * @param attachmentId unique identifier attachment that is uploaded + * @param attachment encrypted attachment that is sent with the submission + * @return the next step where additional data can be added to the submission + */ + WithEncryptedData withEncryptedAttachment(final UUID attachmentId, final String attachment); + + /** + * JSON data as encrypted JWE string. + * + * @param encryptedData encrypted json string + * @return next step to submit the data + */ + WithEncryptedMetadata withEncryptedJsonData(String encryptedData); + + /** + * XML data as encrypted JWE string. + * + * @param encryptedData encrypted xml string + * @return next step to submit the data + */ + WithEncryptedMetadata withEncryptedXmlData(String encryptedData); } public interface WithServiceType { @@ -106,7 +163,7 @@ public final class SenderClient { * @return next step to add attachments * @see <a href="https://fimportal.de/">Search For Service Types</a> */ - WithAttachments withServiceType(String name, String leikaKey); + Submit withServiceType(String name, String leikaKey); /** * Sets a {@link ServiceType} for a submission @@ -117,7 +174,37 @@ public final class SenderClient { * @return next step to add attachments * @see <a href="https://fimportal.de/">Search For Service Types</a> */ - WithAttachments withServiceType(String name, String description, String leikaKey); + Submit withServiceType(String name, String description, String leikaKey); + } + + public interface WithEncryptedData { + + /** + * JSON data as encrypted JWE string. + * + * @param encryptedData encrypted json string + * @return next step to add encrypted metadata + */ + WithEncryptedMetadata withEncryptedJsonData(String encryptedData); + + /** + * XML data as encrypted JWE string. + * + * @param encryptedData encrypted xml string + * @return next step to add encrypted metadata + */ + WithEncryptedMetadata withEncryptedXmlData(String encryptedData); + } + + public interface WithEncryptedMetadata { + + /** + * JSON data as encrypted JWE string. + * + * @param encryptedMetadata encrypted JWE string of the metadata + * @return next step to add a destination + */ + WithDestination withEncryptedMetadata(String encryptedMetadata); } public interface WithData { @@ -128,7 +215,7 @@ public final class SenderClient { * @param data json string * @return next step to submit the data */ - Submit withJsonData(String data); + WithDestination withJsonData(String data); /** * JSON data as string. @@ -136,7 +223,7 @@ public final class SenderClient { * @param data xml string * @return next step to submit the data */ - Submit withXmlData(String data); + WithDestination withXmlData(String data); } @@ -153,346 +240,176 @@ public final class SenderClient { /** * ClientBuilder that implements all steps to guide through the API calls */ - public static class ClientBuilder implements WithDestination, WithServiceType, WithAttachments, WithData, Submit { + public static class ClientBuilder implements + Start, + WithAttachments, WithEncryptedAttachments, + WithData, WithEncryptedData, + WithEncryptedMetadata, + WithDestination, + WithServiceType, + Submit { private final Sender sender; private final Environment environment; private final String metadataSchema; - private DataPayload dataPayload; - private UUID destinationId; - private ServiceType serviceType; - private List<File> attachments = new ArrayList<>(); + private SubmitStrategy submitStrategy; + private SubmissionPayload submissionPayload; public ClientBuilder(final Sender sender, final String metadataSchema, final Environment environment) { this.sender = sender; this.metadataSchema = metadataSchema; this.environment = environment; + this.submissionPayload = new SubmissionPayload(); } @Override - public WithServiceType withDestination(final UUID destinationId) { - this.destinationId = destinationId; + public WithAttachments newSubmission() { + this.submitStrategy = new SendNewSubmissionStrategy(sender, metadataSchema, environment); return this; } @Override - public WithAttachments withServiceType(final String name, final String leikaKey) { - this.serviceType = createServiceTypeWithoutDescription(name, leikaKey); + public WithEncryptedAttachments newSubmissionWithEncryptedData() { + this.submitStrategy = new SendEncryptedSubmissionStrategy(sender); return this; } @Override - public WithAttachments withServiceType(final String name, final String description, final String leikaKey) { - this.serviceType = createServiceTypeWithDescription(name, description, leikaKey); - return this; + public Optional<String> getPublicKey(final UUID destinationId) { + try { + return retrievePublicKey(destinationId); + } catch (final RestApiException | KeyNotRetrievedException e) { + LOGGER.error("Public encryption key could not be retrieved", e); + } + return Optional.empty(); } @Override - public Submit withXmlData(final String data) { - this.dataPayload = getDataPayload(data, MimeType.APPLICATION_XML); + public WithServiceType withDestination(final UUID destinationId) { + this.submissionPayload = this.submissionPayload.withDestinationId(destinationId); return this; } @Override - public Submit withJsonData(final String data) { - this.dataPayload = getDataPayload(data, MimeType.APPLICATION_JSON); + public WithData withAttachments(final List<File> attachments) { + final UUID attachmentId = UUID.randomUUID(); + final List<AttachmentPayload> attachmentPayloads = attachments.stream() + .map(SubmissionUtil.getFileAttachmentPayload(attachmentId)) + .collect(Collectors.toList()); + submissionPayload = submissionPayload.withAttachments(attachmentPayloads); return this; } @Override - public WithData withAttachment(final File attachmentFile) { - this.attachments = attachmentFile != null ? List.of(attachmentFile) : List.of(); + public WithData withAttachment(final File attachment) { + if (attachment != null) { + return withAttachments(List.of(attachment)); + } return this; } @Override - public WithData withAttachments(final List<File> attachmentFiles) { - this.attachments = attachmentFiles; + public WithDestination withJsonData(final String data) { + final DataPayload dataPayload = DataPayload.builder() + .rawData(data == null ? null : data.getBytes(StandardCharsets.UTF_8)) + .mimeType(MimeType.APPLICATION_JSON) + .build(); + submissionPayload = submissionPayload.withData(dataPayload); return this; } @Override - public Optional<SubmitSubmission> submit() { - try { - - if (!isAllNecessaryDataSet()) { - return Optional.empty(); - } - - /** Get encryption key for destination **/ - final Destination destination = sender.getDestination(destinationId); - final RSAKey encryptionKey = sender.getEncryptionKeyForDestination(destinationId, destination.getEncryptionKid()); - final ValidationResult validationResult = sender.validatePublicKey(encryptionKey); - if (validationResult.hasError()) { - LOGGER.warn("The public key is not valid, {}", validationResult.getError().getMessage()); - if (!environment.isSilentKeyValidation()) { - return Optional.empty(); - } - } - - /** Create new submission and announce attachments **/ - final List<AttachmentPayload> attachmentPayloads = encryptAndHashFiles(encryptionKey, attachments); - final List<UUID> attachmentIdsToAnnounce = toAttachmentIds(attachmentPayloads); - final CreateSubmission newSubmission = createSubmission(attachmentIdsToAnnounce); - final SubmissionForPickup announcedSubmission = sender.createSubmission(newSubmission); - final UUID submissionId = announcedSubmission.getSubmissionId(); - uploadAttachments(attachmentPayloads, submissionId); - - /** Prepare submit submission with announced submission id **/ - final SubmitSubmission submission = new SubmitSubmission(); - submission.setSubmissionId(submissionId); - - /** Create attachment hashes **/ - final List<Attachment> hashedAttachments = toHashedAttachments(attachmentPayloads); - - /** Set encrypted metadata with data payload and attachments **/ - LOGGER.info("Adding data payload with mime-type {} to submission", this.dataPayload.mimeType); - final DataPayload dataToSend = getEncryptedData(this.dataPayload, destination, encryptionKey); - final Metadata metadata = createMetadata(hashedAttachments, dataToSend); - final ValidationResult validatedMetadata = sender.validateMetadata(metadata, metadataSchema); - if (validatedMetadata.hasError()) { - LOGGER.error("Metadata does not match schema", validatedMetadata.getError()); - sender.rejectSubmission(submissionId, destinationId, announcedSubmission.getCaseId()); - return Optional.empty(); - } - submission.setEncryptedData(dataToSend.getEncryptedData()); - submission.setEncryptedMetadata(sender.encryptObject(encryptionKey, metadata)); - - /** submit submission **/ - sender.sendSubmission(submission); - LOGGER.info("SUCCESSFULLY HANDED IN SUBMISSION !"); - return Optional.of(submission); - - } catch (final EncryptionException e) { - LOGGER.error("Encrypting submission failed", e); - } catch (final RestApiException e) { - LOGGER.error("Sending submission failed", e); - } catch (final SchemaNotFoundException e) { - LOGGER.error("Required schema to send valid submission not found", e); - } catch (final SubmissionNotCreatedException e) { - LOGGER.error("Failed to announce new submission", e); - } catch (final KeyNotRetrievedException e) { - LOGGER.error("Getting encryption key for destination {} failed", destinationId, e); - } catch (final AttachmentCreationException e) { - LOGGER.error("Reading file failed. Attachment will not be created.", e); - } - return Optional.empty(); - } - - private DataPayload getEncryptedData(final DataPayload dataPayload, final Destination destination, final RSAKey encryptionKey) { - final List<SubmissionSchema> submissionSchemas = getSubmissionSchemasFromDestination(destination); - final Optional<URI> schemaUriForMimeType = getSchemaUriForMimeType(submissionSchemas, dataPayload.mimeType); - if (schemaUriForMimeType.isEmpty()) { - throw new SchemaNotFoundException("No data schema for mime-type " + dataPayload.mimeType + " found, please check the allowed type for destination"); - } - final String hashedData = sender.createHash(dataPayload.rawData); - final String encryptedData = sender.encryptBytes(encryptionKey, dataPayload.rawData); - return dataPayload.withSchemaUri(schemaUriForMimeType.get()) - .withEncryptedData(encryptedData) - .withHashedData(hashedData); - } - - private List<Attachment> toHashedAttachments(final List<AttachmentPayload> attachmentPayloads) { - return attachmentPayloads.stream() - .map(this::toHashedAttachment) - .collect(Collectors.toList()); - } - - private CreateSubmission createSubmission(final List<UUID> attachmentIdsToAnnounce) { - return CreateSubmission.builder() - .destinationId(destinationId) - .announcedAttachments(attachmentIdsToAnnounce) - .serviceType(serviceType) - .build(); - } - - private List<UUID> toAttachmentIds(final List<AttachmentPayload> attachmentPayloads) { - return attachmentPayloads.stream() - .map(AttachmentPayload::getAttachmentId) - .collect(Collectors.toList()); - } - - private DataPayload getDataPayload(final String data, final MimeType mimeType) { - if (data == null) { - return null; - } - return DataPayload.builder() - .rawData(data.getBytes(StandardCharsets.UTF_8)) - .mimeType(mimeType) + public WithDestination withXmlData(final String data) { + final DataPayload dataPayload = DataPayload.builder() + .rawData(data == null ? null : data.getBytes(StandardCharsets.UTF_8)) + .mimeType(MimeType.APPLICATION_XML) .build(); + submissionPayload = submissionPayload.withData(dataPayload); + return this; } - private List<SubmissionSchema> getSubmissionSchemasFromDestination(final Destination destination) { - return destination.getServices().stream() - .map(DestinationService::getSubmissionSchemas) - .flatMap(Collection::stream) + @Override + public WithEncryptedData withEncryptedAttachments(final Map<UUID, String> encryptedAttachments) { + final List<AttachmentPayload> attachmentPayloads = encryptedAttachments.entrySet() + .stream() + .map(SubmissionUtil.getEncryptedAttachmentPayload()) .collect(Collectors.toList()); + submissionPayload = submissionPayload.withAttachments(attachmentPayloads); + return this; } - private Optional<URI> getSchemaUriForMimeType(final List<SubmissionSchema> submissionSchemas, final MimeType mimeType) { - return submissionSchemas.stream() - .filter(schema -> schema.getMimeType().equals(mimeType)) - .map(SubmissionSchema::getSchemaUri) - .findFirst(); - } - - private void uploadAttachments(final List<AttachmentPayload> attachmentPayloads, final UUID submissionId) { - if (attachmentPayloads.isEmpty()) { - LOGGER.info("No attachments to upload"); - } else { - LOGGER.info("Uploading {} attachment(s)", attachmentPayloads.size()); - } - for (final AttachmentPayload payload : attachmentPayloads) { - sender.uploadAttachment(submissionId, payload.attachmentId, payload.encryptedData); - } - } - - private ServiceType createServiceTypeWithDescription(final String name, final String description, final String leikaKey) { - final var serviceTypeModel = new ServiceType(); - serviceTypeModel.setName(name); - serviceTypeModel.setDescription(description); - serviceTypeModel.setIdentifier(leikaKey); - return serviceTypeModel; - } - - private ServiceType createServiceTypeWithoutDescription(final String name, final String leikaKey) { - final var serviceTypeModel = new ServiceType(); - serviceTypeModel.setName(name); - serviceTypeModel.setIdentifier(leikaKey); - return serviceTypeModel; - } - - private Data createData(final DataPayload dataPayload) { - final var hash = new Hash(); - hash.setContent(dataPayload.hashedData); - hash.setDataSignatureType(DataSignatureType.SHA_512); - - final var submissionSchema = new SubmissionSchema(); - submissionSchema.setMimeType(dataPayload.mimeType); - submissionSchema.setSchemaUri(dataPayload.schemaUri); - - final var data = new Data(); - data.setSubmissionSchema(submissionSchema); - data.setHash(hash); - return data; - } - - private Metadata createMetadata(final List<Attachment> attachments, final DataPayload dataPayload) { - final var contentStructure = new ContentStructure(); - contentStructure.setAttachments(attachments); - contentStructure.setData(createData(dataPayload)); - - final var metadata = new Metadata(); - metadata.setContentStructure(contentStructure); - metadata.setPublicServiceType(getPublicServiceType()); - return metadata; - } - - private PublicServiceType getPublicServiceType() { - final var publicServiceType = new PublicServiceType(); - publicServiceType.setIdentifier(serviceType.getIdentifier()); - publicServiceType.setName(serviceType.getName()); - if (serviceType.getDescription() != null) { - publicServiceType.setDescription(serviceType.getDescription()); + @Override + public WithEncryptedData withEncryptedAttachment(final UUID attachmentId, final String attachment) { + if (attachment != null) { + return withEncryptedAttachments(Map.of(attachmentId, attachment)); } - return publicServiceType; + return this; } - private List<AttachmentPayload> encryptAndHashFiles(final RSAKey encryptionKey, final List<File> attachmentFiles) { - return attachmentFiles.stream() - .map(this::readRawData) - .map(this::hashBytes) - .map(payload -> encryptBytes(encryptionKey, payload)) - .map(this::setMimeType) - .collect(Collectors.toList()); + @Override + public WithEncryptedMetadata withEncryptedJsonData(final String encryptedData) { + final DataPayload dataPayload = DataPayload.builder() + .encryptedData(encryptedData) + .mimeType(MimeType.APPLICATION_JSON) + .build(); + submissionPayload = submissionPayload.withData(dataPayload); + return this; } - private AttachmentPayload readRawData(final File file) { - try { - final byte[] rawData = Files.readAllBytes(Paths.get(file.getPath())); - return AttachmentPayload.builder().file(file).rawData(rawData).build(); - } catch (final IOException e) { - throw new AttachmentCreationException("Attachment '" + file.getAbsolutePath() + "' could not be read ", e); - } + @Override + public WithEncryptedMetadata withEncryptedXmlData(final String encryptedData) { + final DataPayload dataPayload = DataPayload.builder() + .encryptedData(encryptedData) + .mimeType(MimeType.APPLICATION_XML) + .build(); + submissionPayload = submissionPayload.withData(dataPayload); + return this; } - private Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) { - final var attachment = new Attachment(); - attachment.setAttachmentId(attachmentPayload.attachmentId); - attachment.setPurpose(Purpose.ATTACHMENT); - attachment.setFilename(attachmentPayload.file.getName()); - attachment.setMimeType(attachmentPayload.mimeType); - - final var hash = new Hash__1(); - hash.setContent(attachmentPayload.hashedData); - hash.setAttachmentSignatureType(AttachmentSignatureType.SHA_512); - attachment.setHash(hash); - return attachment; + @Override + public WithDestination withEncryptedMetadata(final String encryptedMetadata) { + submissionPayload = submissionPayload.withEncryptedMetadata(encryptedMetadata); + return this; } - private AttachmentPayload encryptBytes(final RSAKey encryptionKey, final AttachmentPayload attachmentPayload) { - final String encryptedAttachment = sender.encryptBytes(encryptionKey, attachmentPayload.rawData); - return attachmentPayload.withEncryptedData(encryptedAttachment); + @Override + public Submit withServiceType(final String name, final String leikaKey) { + final ServiceTypePayload serviceTypePayload = ServiceTypePayload.builder() + .name(name) + .leikaKey(leikaKey) + .build(); + submissionPayload = submissionPayload.withServiceTypePayLoad(serviceTypePayload); + return this; } - private AttachmentPayload hashBytes(final AttachmentPayload attachmentPayload) { - final String hashedBytes = sender.createHash(attachmentPayload.rawData); - return attachmentPayload.withHashedData(hashedBytes); + @Override + public Submit withServiceType(final String name, final String description, final String leikaKey) { + final ServiceTypePayload serviceTypePayload = ServiceTypePayload.builder() + .name(name) + .description(description) + .leikaKey(leikaKey) + .build(); + submissionPayload = submissionPayload.withServiceTypePayLoad(serviceTypePayload); + return this; } - private AttachmentPayload setMimeType(final AttachmentPayload attachmentPayload) { - final File file = attachmentPayload.file; - String mimeType; - try { - final Tika mimeTypeGuesser = new Tika(); - mimeType = mimeTypeGuesser.detect(file); - } catch (final IOException e) { - mimeType = URLConnection.guessContentTypeFromName(file.getName()); - } - LOGGER.info("Detected attachment mime-type {}", mimeType); - return attachmentPayload.withMimeType(mimeType); + @Override + public Optional<SubmitSubmission> submit() { + return submitStrategy.send(submissionPayload); } - private boolean isAllNecessaryDataSet() { - if (this.dataPayload == null) { - LOGGER.error("Data is mandatory, but was null."); - return false; - } else if (this.serviceType.getName() == null) { - LOGGER.error("Service type name is mandatory, but was null."); - return false; - } else if (this.serviceType.getIdentifier() == null) { - LOGGER.error("Service type identifier is mandatory, but was null."); - return false; - } else if (this.destinationId == null) { - LOGGER.error("DestinationId is mandatory, but was null."); - return false; - } else { - return true; + private Optional<String> retrievePublicKey(final UUID destinationId) { + final Destination destination = sender.getDestination(destinationId); + final RSAKey encryptionKey = sender.getEncryptionKeyForDestination(destinationId, destination.getEncryptionKid()); + final ValidationResult validationResult = sender.validatePublicKey(encryptionKey); + if (validationResult.hasError()) { + LOGGER.warn("The public key is not valid, {}", validationResult.getError().getMessage()); + if (!environment.isSilentKeyValidation()) { + return Optional.empty(); + } } - } - - @With - @Getter - @Builder - private static class AttachmentPayload { - private File file; - private byte[] rawData; - private String hashedData; - private String encryptedData; - private String mimeType; - private final UUID attachmentId = UUID.randomUUID(); - } - - @With - @Getter - @Builder - private static class DataPayload { - private byte[] rawData; - private String hashedData; - private String encryptedData; - private MimeType mimeType; - private URI schemaUri; + return Optional.of(encryptionKey.toJSONString()); } } } diff --git a/client/src/main/java/de/fitko/fitconnect/client/SubscriberClient.java b/client/src/main/java/de/fitko/fitconnect/client/SubscriberClient.java index f471e6260..c6ebac7d7 100644 --- a/client/src/main/java/de/fitko/fitconnect/client/SubscriberClient.java +++ b/client/src/main/java/de/fitko/fitconnect/client/SubscriberClient.java @@ -7,15 +7,12 @@ import de.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; import de.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentWithData; import de.fitko.fitconnect.api.domain.model.metadata.attachment.signature.Hash__1; import de.fitko.fitconnect.api.domain.model.metadata.data.Data; -import de.fitko.fitconnect.api.domain.model.metadata.data.MimeType; import de.fitko.fitconnect.api.domain.model.submission.Submission; import de.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import de.fitko.fitconnect.api.domain.validation.ValidationResult; import de.fitko.fitconnect.api.exceptions.*; import de.fitko.fitconnect.api.services.Subscriber; -import lombok.Builder; -import lombok.Getter; -import lombok.ToString; +import de.fitko.fitconnect.client.model.ReceivedSubmission; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -244,12 +241,4 @@ public final class SubscriberClient { } } - @Getter - @Builder - @ToString - public static class ReceivedSubmission { - String data; - MimeType mimeType; - List<AttachmentWithData> attachments; - } } diff --git a/client/src/main/java/de/fitko/fitconnect/client/cmd/CommandLineClient.java b/client/src/main/java/de/fitko/fitconnect/client/cmd/CommandLineClient.java index 09c063b16..4b50b75e3 100644 --- a/client/src/main/java/de/fitko/fitconnect/client/cmd/CommandLineClient.java +++ b/client/src/main/java/de/fitko/fitconnect/client/cmd/CommandLineClient.java @@ -3,6 +3,7 @@ package de.fitko.fitconnect.client.cmd; import com.beust.jcommander.JCommander; import com.beust.jcommander.ParameterException; import de.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentWithData; +import de.fitko.fitconnect.client.model.ReceivedSubmission; import de.fitko.fitconnect.client.SenderClient; import de.fitko.fitconnect.client.SubscriberClient; import de.fitko.fitconnect.client.cmd.commands.ListAllSubmissionsCommand; @@ -17,6 +18,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.stream.Collectors; import static de.fitko.fitconnect.client.cmd.util.AttachmentDataType.JSON; @@ -32,10 +34,10 @@ public class CommandLineClient { private final JCommander jc; - private final SenderClient.WithDestination senderClient; + private final SenderClient.Start senderClient; private final SubscriberClient.RequestSubmission subscriberClient; - public CommandLineClient(final SenderClient.WithDestination senderClient, + public CommandLineClient(final SenderClient.Start senderClient, final SubscriberClient.RequestSubmission subscriberClient) { this.sendSubmissionCommand = new SendSubmissionCommand(); @@ -108,17 +110,32 @@ public class CommandLineClient { private void sendSubmission(final SendSubmissionCommand sendSubmissionCommand) { LOGGER.info("Sending new submission to destination {}", sendSubmissionCommand.destinationId); - final var client = senderClient - .withDestination(sendSubmissionCommand.destinationId) - .withServiceType(sendSubmissionCommand.serviceName, sendSubmissionCommand.leikaKey) - .withAttachments(sendSubmissionCommand.attachments.stream().map(File::new).collect(Collectors.toList())); + final List<File> files = sendSubmissionCommand.attachments.stream().map(File::new).collect(Collectors.toList()); if (sendSubmissionCommand.dataType == JSON) { - client.withJsonData(sendSubmissionCommand.data).submit(); + sendWithJsonData(sendSubmissionCommand, files); } else if (sendSubmissionCommand.dataType == XML) { - client.withXmlData(sendSubmissionCommand.data).submit(); + sendWithXmlData(sendSubmissionCommand, files); } } + private void sendWithXmlData(final SendSubmissionCommand sendSubmissionCommand, final List<File> files) { + senderClient.newSubmission() + .withAttachments(files) + .withXmlData(sendSubmissionCommand.data) + .withDestination(sendSubmissionCommand.destinationId) + .withServiceType(sendSubmissionCommand.serviceName, sendSubmissionCommand.leikaKey) + .submit(); + } + + private void sendWithJsonData(final SendSubmissionCommand sendSubmissionCommand, final List<File> files) { + senderClient.newSubmission() + .withAttachments(files) + .withJsonData(sendSubmissionCommand.data) + .withDestination(sendSubmissionCommand.destinationId) + .withServiceType(sendSubmissionCommand.serviceName, sendSubmissionCommand.leikaKey) + .submit(); + } + private String getTargetFolderPath(final ListOneSubmissionCommand listOneSubmissionCommand) { if (listOneSubmissionCommand.targetFolder != null) { return listOneSubmissionCommand.targetFolder + "/" + listOneSubmissionCommand.submissionId; @@ -127,7 +144,7 @@ public class CommandLineClient { } } - private void writeData(final SubscriberClient.ReceivedSubmission submissionData, final String dataDirPath) throws IOException { + private void writeData(final ReceivedSubmission submissionData, final String dataDirPath) throws IOException { LOGGER.info("Creating data directory for submission in {}", dataDirPath); Files.createDirectories(Path.of(dataDirPath)); diff --git a/client/src/main/java/de/fitko/fitconnect/client/factory/ClientFactory.java b/client/src/main/java/de/fitko/fitconnect/client/factory/ClientFactory.java index 48a4a8cb9..291357f70 100644 --- a/client/src/main/java/de/fitko/fitconnect/client/factory/ClientFactory.java +++ b/client/src/main/java/de/fitko/fitconnect/client/factory/ClientFactory.java @@ -58,7 +58,7 @@ public final class ClientFactory { * * @return the sender client */ - public static SenderClient.WithDestination senderClient() { + public static SenderClient.Start senderClient() { final ApplicationConfig config = loadConfig(); return senderClient(config); } @@ -68,7 +68,7 @@ public final class ClientFactory { * * @return the sender client */ - public static SenderClient.WithDestination senderClient(final ApplicationConfig config) { + public static SenderClient.Start senderClient(final ApplicationConfig config) { LOGGER.info(SENDER_BANNER); LOGGER.info("Initializing sender client ..."); final Sender sender = getSender(config); diff --git a/client/src/main/java/de/fitko/fitconnect/client/model/AttachmentPayload.java b/client/src/main/java/de/fitko/fitconnect/client/model/AttachmentPayload.java new file mode 100644 index 000000000..bbf351500 --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/model/AttachmentPayload.java @@ -0,0 +1,20 @@ +package de.fitko.fitconnect.client.model; + +import lombok.Builder; +import lombok.Getter; +import lombok.With; + +import java.io.File; +import java.util.UUID; + +@With +@Getter +@Builder +public class AttachmentPayload { + private File file; + private byte[] rawData; + private String hashedData; + private String encryptedData; + private String mimeType; + private UUID attachmentId; +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/model/DataPayload.java b/client/src/main/java/de/fitko/fitconnect/client/model/DataPayload.java new file mode 100644 index 000000000..cdd45adaa --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/model/DataPayload.java @@ -0,0 +1,19 @@ +package de.fitko.fitconnect.client.model; + +import de.fitko.fitconnect.api.domain.model.metadata.data.MimeType; +import lombok.Builder; +import lombok.Getter; +import lombok.With; + +import java.net.URI; + +@With +@Getter +@Builder +public class DataPayload { + private byte[] rawData; + private String hashedData; + private String encryptedData; + private MimeType mimeType; + private URI schemaUri; +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/model/ReceivedSubmission.java b/client/src/main/java/de/fitko/fitconnect/client/model/ReceivedSubmission.java new file mode 100644 index 000000000..dd3dc5b15 --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/model/ReceivedSubmission.java @@ -0,0 +1,18 @@ +package de.fitko.fitconnect.client.model; + +import de.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentWithData; +import de.fitko.fitconnect.api.domain.model.metadata.data.MimeType; +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@Getter +@Builder +@ToString +public class ReceivedSubmission { + private String data; + private MimeType mimeType; + private List<AttachmentWithData> attachments; +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/model/ServiceTypePayload.java b/client/src/main/java/de/fitko/fitconnect/client/model/ServiceTypePayload.java new file mode 100644 index 000000000..26f505e2e --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/model/ServiceTypePayload.java @@ -0,0 +1,12 @@ +package de.fitko.fitconnect.client.model; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ServiceTypePayload { + private String name; + private String description; + private String leikaKey; +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/model/SubmissionPayload.java b/client/src/main/java/de/fitko/fitconnect/client/model/SubmissionPayload.java new file mode 100644 index 000000000..b5f57a10c --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/model/SubmissionPayload.java @@ -0,0 +1,26 @@ +package de.fitko.fitconnect.client.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.With; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@Data +@With +@AllArgsConstructor +@NoArgsConstructor +public class SubmissionPayload { + + private UUID destinationId; + private DataPayload data; + private String encryptedMetadata; + private ServiceTypePayload serviceTypePayLoad; + + // Optional - default empty List + private List<AttachmentPayload> attachments = Collections.emptyList(); + private List<String> encryptedAttachments = Collections.emptyList(); +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/strategies/SendEncryptedSubmissionStrategy.java b/client/src/main/java/de/fitko/fitconnect/client/strategies/SendEncryptedSubmissionStrategy.java new file mode 100644 index 000000000..5c08300d2 --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/strategies/SendEncryptedSubmissionStrategy.java @@ -0,0 +1,92 @@ +package de.fitko.fitconnect.client.strategies; + +import de.fitko.fitconnect.api.domain.model.submission.CreateSubmission; +import de.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; +import de.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; +import de.fitko.fitconnect.api.exceptions.RestApiException; +import de.fitko.fitconnect.api.exceptions.SubmissionNotCreatedException; +import de.fitko.fitconnect.api.services.Sender; +import de.fitko.fitconnect.client.model.AttachmentPayload; +import de.fitko.fitconnect.client.model.SubmissionPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static de.fitko.fitconnect.client.util.SubmissionUtil.buildSubmissionToAnnounce; +import static de.fitko.fitconnect.client.util.SubmissionUtil.buildSubmitSubmission; + +public class SendEncryptedSubmissionStrategy implements SubmitStrategy { + + private static final Logger LOGGER = LoggerFactory.getLogger(SendEncryptedSubmissionStrategy.class); + + private final Sender sender; + + public SendEncryptedSubmissionStrategy(final Sender sender) { + this.sender = sender; + } + + @Override + public Optional<SubmitSubmission> send(final SubmissionPayload submissionPayload) { + + if (!hasValidPayload(submissionPayload)) { + return Optional.empty(); + } + + try { + final UUID submissionId = announceNewSubmission(submissionPayload); + final SubmitSubmission submission = buildSubmitSubmission(submissionPayload, submissionId); + uploadAttachments(submissionPayload.getAttachments(), submissionId); + sender.sendSubmission(submission); + LOGGER.info("SUCCESSFULLY HANDED IN SUBMISSION !"); + return Optional.of(submission); + + } catch (final RestApiException e) { + LOGGER.error("Sending submission failed", e); + } catch (final SubmissionNotCreatedException e) { + LOGGER.error("Failed to announce new submission", e); + } + return Optional.empty(); + } + + private UUID announceNewSubmission(final SubmissionPayload submissionPayload) { + final CreateSubmission submissionToAnnounce = buildSubmissionToAnnounce(submissionPayload); + final SubmissionForPickup announcedSubmission = sender.createSubmission(submissionToAnnounce); + return announcedSubmission.getSubmissionId(); + } + + private void uploadAttachments(final List<AttachmentPayload> attachmentPayloads, final UUID submissionId) { + if (attachmentPayloads.isEmpty()) { + LOGGER.info("No attachments to upload"); + } else { + LOGGER.info("Uploading {} attachment(s)", attachmentPayloads.size()); + } + for (final AttachmentPayload payload : attachmentPayloads) { + sender.uploadAttachment(submissionId, payload.getAttachmentId(), payload.getEncryptedData()); + } + } + + @Override + public boolean hasValidPayload(final SubmissionPayload submissionPayload) { + if (submissionPayload.getData().getEncryptedData() == null) { + LOGGER.error("Data is mandatory, but was null."); + return false; + } else if (submissionPayload.getEncryptedMetadata() == null) { + LOGGER.error("Metadata is mandatory, but was null."); + return false; + } else if (submissionPayload.getServiceTypePayLoad().getName() == null) { + LOGGER.error("Service type name is mandatory, but was null."); + return false; + } else if (submissionPayload.getServiceTypePayLoad().getLeikaKey() == null) { + LOGGER.error("Service type identifier is mandatory, but was null."); + return false; + } else if (submissionPayload.getDestinationId() == null) { + LOGGER.error("DestinationId is mandatory, but was null."); + return false; + } else { + return true; + } + } +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/strategies/SendNewSubmissionStrategy.java b/client/src/main/java/de/fitko/fitconnect/client/strategies/SendNewSubmissionStrategy.java new file mode 100644 index 000000000..b4856f1f0 --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/strategies/SendNewSubmissionStrategy.java @@ -0,0 +1,234 @@ +package de.fitko.fitconnect.client.strategies; + +import com.nimbusds.jose.jwk.RSAKey; +import de.fitko.fitconnect.api.config.Environment; +import de.fitko.fitconnect.api.domain.model.destination.Destination; +import de.fitko.fitconnect.api.domain.model.destination.DestinationService; +import de.fitko.fitconnect.api.domain.model.metadata.Metadata; +import de.fitko.fitconnect.api.domain.model.metadata.PublicServiceType; +import de.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; +import de.fitko.fitconnect.api.domain.model.metadata.data.Data; +import de.fitko.fitconnect.api.domain.model.metadata.data.MimeType; +import de.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; +import de.fitko.fitconnect.api.domain.model.submission.CreateSubmission; +import de.fitko.fitconnect.api.domain.model.submission.ServiceType; +import de.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; +import de.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; +import de.fitko.fitconnect.api.domain.validation.ValidationResult; +import de.fitko.fitconnect.api.exceptions.*; +import de.fitko.fitconnect.api.services.Sender; +import de.fitko.fitconnect.client.model.AttachmentPayload; +import de.fitko.fitconnect.client.model.DataPayload; +import de.fitko.fitconnect.client.model.ServiceTypePayload; +import de.fitko.fitconnect.client.model.SubmissionPayload; +import de.fitko.fitconnect.client.util.SubmissionUtil; +import org.apache.tika.Tika; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +public class SendNewSubmissionStrategy implements SubmitStrategy { + + private static final Logger LOGGER = LoggerFactory.getLogger(SendNewSubmissionStrategy.class); + + private final Sender sender; + private final String metadataSchema; + private final Environment environment; + + public SendNewSubmissionStrategy(final Sender sender, final String metadataSchema, final Environment environment) { + this.sender = sender; + this.metadataSchema = metadataSchema; + this.environment = environment; + } + + @Override + public Optional<SubmitSubmission> send(final SubmissionPayload submissionPayload) { + + if (!hasValidPayload(submissionPayload)) return Optional.empty(); + + final UUID destinationId = submissionPayload.getDestinationId(); + final ServiceTypePayload serviceTypePayLoad = submissionPayload.getServiceTypePayLoad(); + final DataPayload userDataPayload = submissionPayload.getData(); + final List<AttachmentPayload> attachments = submissionPayload.getAttachments(); + + try { + + /** Get encryption key for destination **/ + final Destination destination = sender.getDestination(destinationId); + final RSAKey encryptionKey = sender.getEncryptionKeyForDestination(destinationId, destination.getEncryptionKid()); + final ValidationResult validationResult = sender.validatePublicKey(encryptionKey); + if (validationResult.hasError()) { + LOGGER.warn("The public key is not valid, {}", validationResult.getError().getMessage()); + if (!environment.isSilentKeyValidation()) { + return Optional.empty(); + } + } + + /** Create new submission and announce attachments **/ + final List<AttachmentPayload> encryptedAttachments = encryptAndHashAttachments(encryptionKey, attachments); + final CreateSubmission newSubmission = buildSubmissionToAnnounce(destinationId, serviceTypePayLoad, encryptedAttachments); + final SubmissionForPickup announcedSubmission = sender.createSubmission(newSubmission); + + final UUID submissionId = announcedSubmission.getSubmissionId(); + uploadAttachments(encryptedAttachments, submissionId); + + /** Create attachment metadata including hashes **/ + final List<Attachment> attachmentMetadata = SubmissionUtil.toAttachmentMetadata(encryptedAttachments); + + /** Build encrypted data payload from user data**/ + LOGGER.info("Adding data payload with mime-type {} to submission", userDataPayload.getMimeType()); + final DataPayload encryptedDataPayload = encryptDataPayload(userDataPayload, destination, encryptionKey); + + /** Set encrypted metadata with data payload and attachments **/ + final Data dataToSend = SubmissionUtil.buildData(encryptedDataPayload); + final PublicServiceType publicServiceType = SubmissionUtil.buildPublicServiceType(serviceTypePayLoad); + final Metadata metadata = SubmissionUtil.buildMetadata(attachmentMetadata, dataToSend, publicServiceType); + + final ValidationResult validatedMetadata = sender.validateMetadata(metadata, metadataSchema); + if (validatedMetadata.hasError()) { + LOGGER.error("Metadata does not match schema", validatedMetadata.getError()); + sender.rejectSubmission(submissionId, destinationId, announcedSubmission.getCaseId()); + return Optional.empty(); + } + + /** Prepare submit submission with announced submission id **/ + final SubmitSubmission submission = new SubmitSubmission(); + submission.setSubmissionId(submissionId); + submission.setEncryptedData(encryptedDataPayload.getEncryptedData()); + submission.setEncryptedMetadata(sender.encryptObject(encryptionKey, metadata)); + + /** Submit submission **/ + sender.sendSubmission(submission); + LOGGER.info("SUCCESSFULLY HANDED IN SUBMISSION !"); + return Optional.of(submission); + + } catch (final EncryptionException e) { + LOGGER.error("Encrypting submission failed", e); + } catch (final RestApiException e) { + LOGGER.error("Sending submission failed", e); + } catch (final SchemaNotFoundException e) { + LOGGER.error("Required schema to send valid submission not found", e); + } catch (final SubmissionNotCreatedException e) { + LOGGER.error("Failed to announce new submission", e); + } catch (final KeyNotRetrievedException e) { + LOGGER.error("Getting encryption key for destination {} failed", destinationId, e); + } catch (final AttachmentCreationException e) { + LOGGER.error("Reading file failed. Attachment will not be created.", e); + } + return Optional.empty(); + } + + @Override + public boolean hasValidPayload(final SubmissionPayload submissionPayload) { + if (submissionPayload.getData().getRawData() == null) { + LOGGER.error("Data is mandatory, but was null."); + return false; + } else if (submissionPayload.getServiceTypePayLoad().getName() == null) { + LOGGER.error("Service type name is mandatory, but was null."); + return false; + } else if (submissionPayload.getServiceTypePayLoad().getLeikaKey() == null) { + LOGGER.error("Service type identifier is mandatory, but was null."); + return false; + } else if (submissionPayload.getDestinationId() == null) { + LOGGER.error("DestinationId is mandatory, but was null."); + return false; + } else { + return true; + } + } + + private DataPayload encryptDataPayload(final DataPayload dataPayload, final Destination destination, final RSAKey encryptionKey) { + final List<SubmissionSchema> submissionSchemas = getSubmissionSchemasFromDestination(destination); + final Optional<URI> schemaUriForMimeType = getSchemaUriForMimeType(submissionSchemas, dataPayload.getMimeType()); + if (schemaUriForMimeType.isEmpty()) { + throw new SchemaNotFoundException("No data schema for mime-type " + dataPayload.getMimeType() + " found, please check the allowed type for destination"); + } + final String hashedData = sender.createHash(dataPayload.getRawData()); + final String encryptedData = sender.encryptBytes(encryptionKey, dataPayload.getRawData()); + return dataPayload.withSchemaUri(schemaUriForMimeType.get()) + .withEncryptedData(encryptedData) + .withHashedData(hashedData); + } + + private List<SubmissionSchema> getSubmissionSchemasFromDestination(final Destination destination) { + return destination.getServices().stream() + .map(DestinationService::getSubmissionSchemas) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private Optional<URI> getSchemaUriForMimeType(final List<SubmissionSchema> submissionSchemas, final MimeType mimeType) { + return submissionSchemas.stream() + .filter(schema -> schema.getMimeType().equals(mimeType)) + .map(SubmissionSchema::getSchemaUri) + .findFirst(); + } + + private void uploadAttachments(final List<AttachmentPayload> attachmentPayloads, final UUID submissionId) { + if (attachmentPayloads.isEmpty()) { + LOGGER.info("No attachments to upload"); + } else { + LOGGER.info("Uploading {} attachment(s)", attachmentPayloads.size()); + } + for (final AttachmentPayload payload : attachmentPayloads) { + sender.uploadAttachment(submissionId, payload.getAttachmentId(), payload.getEncryptedData()); + } + } + + private CreateSubmission buildSubmissionToAnnounce(final UUID destinationId, final ServiceTypePayload serviceTypePayLoad, final List<AttachmentPayload> encryptedAttachments) { + final ServiceType serviceType = SubmissionUtil.buildServiceType(serviceTypePayLoad); + final List<UUID> attachmentIdsToAnnounce = SubmissionUtil.toAttachmentIds(encryptedAttachments); + return SubmissionUtil.createSubmission(destinationId, serviceType, attachmentIdsToAnnounce); + } + + private List<AttachmentPayload> encryptAndHashAttachments(final RSAKey encryptionKey, final List<AttachmentPayload> attachments) { + return attachments.stream() + .filter(Objects::nonNull) + .map(this::readRawData) + .map(this::hashBytes) + .map(payload -> encryptBytes(encryptionKey, payload)) + .map(this::setMimeType) + .collect(Collectors.toList()); + } + + private AttachmentPayload readRawData(final AttachmentPayload attachmentPayload) { + final File file = attachmentPayload.getFile(); + try { + final byte[] rawData = Files.readAllBytes(Paths.get(file.getPath())); + return attachmentPayload.withRawData(rawData); + } catch (final IOException e) { + throw new AttachmentCreationException("Attachment '" + file.getAbsolutePath() + "' could not be read ", e); + } + } + + private AttachmentPayload encryptBytes(final RSAKey encryptionKey, final AttachmentPayload attachmentPayload) { + final String encryptedAttachment = sender.encryptBytes(encryptionKey, attachmentPayload.getRawData()); + return attachmentPayload.withEncryptedData(encryptedAttachment); + } + + private AttachmentPayload hashBytes(final AttachmentPayload attachmentPayload) { + final String hashedBytes = sender.createHash(attachmentPayload.getRawData()); + return attachmentPayload.withHashedData(hashedBytes); + } + + private AttachmentPayload setMimeType(final AttachmentPayload attachmentPayload) { + final File file = attachmentPayload.getFile(); + String mimeType; + try { + final Tika mimeTypeGuesser = new Tika(); + mimeType = mimeTypeGuesser.detect(file); + } catch (final IOException e) { + mimeType = URLConnection.guessContentTypeFromName(file.getName()); + } + LOGGER.info("Detected attachment mime-type {}", mimeType); + return attachmentPayload.withMimeType(mimeType); + } +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/strategies/SubmitStrategy.java b/client/src/main/java/de/fitko/fitconnect/client/strategies/SubmitStrategy.java new file mode 100644 index 000000000..92d9e3e55 --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/strategies/SubmitStrategy.java @@ -0,0 +1,30 @@ +package de.fitko.fitconnect.client.strategies; + +import de.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; +import de.fitko.fitconnect.client.model.SubmissionPayload; + +import java.util.Optional; + +/** + * Provides different strategies for assembling, announcing and handing in a new submission. + */ +public interface SubmitStrategy { + + /** + * Send a submission based on specific assumptions on how the submission is assembled. + * + * @param submissionPayload all necessary data for creating and submitting a submission + * + * @return {@link SubmitSubmission}, empty if a validation error or technical error occurred. + */ + Optional<SubmitSubmission> send(SubmissionPayload submissionPayload); + + /** + * Validate the payload to be sufficient for submission. + * + * @param submissionPayload payload of all data needed to create a submission + * + * @return true if all checks apply, false if data is missing + */ + boolean hasValidPayload(SubmissionPayload submissionPayload); +} diff --git a/client/src/main/java/de/fitko/fitconnect/client/util/SubmissionUtil.java b/client/src/main/java/de/fitko/fitconnect/client/util/SubmissionUtil.java new file mode 100644 index 000000000..748fdfc56 --- /dev/null +++ b/client/src/main/java/de/fitko/fitconnect/client/util/SubmissionUtil.java @@ -0,0 +1,139 @@ +package de.fitko.fitconnect.client.util; + +import de.fitko.fitconnect.api.domain.model.metadata.ContentStructure; +import de.fitko.fitconnect.api.domain.model.metadata.Metadata; +import de.fitko.fitconnect.api.domain.model.metadata.PublicServiceType; +import de.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; +import de.fitko.fitconnect.api.domain.model.metadata.attachment.Purpose; +import de.fitko.fitconnect.api.domain.model.metadata.attachment.signature.AttachmentSignatureType; +import de.fitko.fitconnect.api.domain.model.metadata.attachment.signature.Hash__1; +import de.fitko.fitconnect.api.domain.model.metadata.data.Data; +import de.fitko.fitconnect.api.domain.model.metadata.data.DataSignatureType; +import de.fitko.fitconnect.api.domain.model.metadata.data.Hash; +import de.fitko.fitconnect.api.domain.model.metadata.data.SubmissionSchema; +import de.fitko.fitconnect.api.domain.model.submission.CreateSubmission; +import de.fitko.fitconnect.api.domain.model.submission.ServiceType; +import de.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; +import de.fitko.fitconnect.client.model.AttachmentPayload; +import de.fitko.fitconnect.client.model.DataPayload; +import de.fitko.fitconnect.client.model.ServiceTypePayload; +import de.fitko.fitconnect.client.model.SubmissionPayload; +import de.fitko.fitconnect.core.util.Strings; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +public final class SubmissionUtil { + + public static CreateSubmission createSubmission(final UUID destinationId, final ServiceType serviceType, final List<UUID> attachmentIdsToAnnounce) { + return CreateSubmission.builder() + .destinationId(destinationId) + .announcedAttachments(attachmentIdsToAnnounce) + .serviceType(serviceType) + .build(); + } + + public static ServiceType buildServiceType(final ServiceTypePayload serviceTypePayload) { + final var serviceType = new ServiceType(); + serviceType.setName(serviceTypePayload.getName()); + serviceType.setIdentifier(serviceTypePayload.getLeikaKey()); + if (!Strings.isNullOrEmpty(serviceTypePayload.getDescription())) { + serviceType.setDescription(serviceTypePayload.getDescription()); + } + return serviceType; + } + + public static PublicServiceType buildPublicServiceType(final ServiceTypePayload serviceTypePayload) { + final var publicServiceType = new PublicServiceType(); + publicServiceType.setIdentifier(serviceTypePayload.getLeikaKey()); + publicServiceType.setName(serviceTypePayload.getName()); + if (serviceTypePayload.getDescription() != null) { + publicServiceType.setDescription(serviceTypePayload.getDescription()); + } + return publicServiceType; + } + + public static Data buildData(final DataPayload dataPayload) { + final var hash = new Hash(); + hash.setContent(dataPayload.getHashedData()); + hash.setDataSignatureType(DataSignatureType.SHA_512); + + final var submissionSchema = new SubmissionSchema(); + submissionSchema.setMimeType(dataPayload.getMimeType()); + submissionSchema.setSchemaUri(dataPayload.getSchemaUri()); + + final var data = new Data(); + data.setSubmissionSchema(submissionSchema); + data.setHash(hash); + return data; + } + + public static Metadata buildMetadata(final List<Attachment> attachments, final Data data, final PublicServiceType publicServiceType) { + final var contentStructure = new ContentStructure(); + contentStructure.setAttachments(attachments); + contentStructure.setData(data); + + final var metadata = new Metadata(); + metadata.setContentStructure(contentStructure); + metadata.setPublicServiceType(publicServiceType); + return metadata; + } + + public static CreateSubmission buildSubmissionToAnnounce(final SubmissionPayload submissionPayload) { + final ServiceType serviceType = buildServiceType(submissionPayload.getServiceTypePayLoad()); + final List<UUID> attachmentIds = toAttachmentIds(submissionPayload.getAttachments()); + return createSubmission(submissionPayload.getDestinationId(), serviceType, attachmentIds); + } + + public static SubmitSubmission buildSubmitSubmission(final SubmissionPayload submissionPayload, final UUID submissionId) { + final SubmitSubmission submission = new SubmitSubmission(); + submission.setSubmissionId(submissionId); + submission.setEncryptedData(submissionPayload.getData().getEncryptedData()); + submission.setEncryptedMetadata(submissionPayload.getEncryptedMetadata()); + return submission; + } + + public static Attachment toHashedAttachment(final AttachmentPayload attachmentPayload) { + final var attachment = new Attachment(); + attachment.setAttachmentId(attachmentPayload.getAttachmentId()); + attachment.setPurpose(Purpose.ATTACHMENT); + attachment.setFilename(attachmentPayload.getFile().getName()); + attachment.setMimeType(attachmentPayload.getMimeType()); + + final var hash = new Hash__1(); + hash.setContent(attachmentPayload.getHashedData()); + hash.setAttachmentSignatureType(AttachmentSignatureType.SHA_512); + attachment.setHash(hash); + return attachment; + } + + public static List<UUID> toAttachmentIds(final List<AttachmentPayload> attachmentPayloads) { + return attachmentPayloads.stream() + .map(AttachmentPayload::getAttachmentId) + .collect(Collectors.toList()); + } + + public static List<Attachment> toAttachmentMetadata(final List<AttachmentPayload> attachmentPayloads) { + return attachmentPayloads.stream() + .map(SubmissionUtil::toHashedAttachment) + .collect(Collectors.toList()); + } + + public static Function<Map.Entry<UUID, String>, AttachmentPayload> getEncryptedAttachmentPayload() { + return attachment -> AttachmentPayload.builder() + .encryptedData(attachment.getValue()) + .attachmentId(attachment.getKey()) + .build(); + } + + public static Function<File, AttachmentPayload> getFileAttachmentPayload(final UUID attachmentId) { + return file -> AttachmentPayload.builder() + .file(file) + .attachmentId(attachmentId) + .build(); + } +} diff --git a/client/src/test/java/de/fitko/fitconnect/client/ClientIntegrationTest.java b/client/src/test/java/de/fitko/fitconnect/client/ClientIntegrationTest.java index bc9c7cca8..885b3e3e3 100644 --- a/client/src/test/java/de/fitko/fitconnect/client/ClientIntegrationTest.java +++ b/client/src/test/java/de/fitko/fitconnect/client/ClientIntegrationTest.java @@ -1,12 +1,33 @@ package de.fitko.fitconnect.client; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.jwk.RSAKey; import de.fitko.fitconnect.api.config.*; +import de.fitko.fitconnect.api.domain.model.metadata.Metadata; +import de.fitko.fitconnect.api.domain.model.metadata.PublicServiceType; +import de.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; +import de.fitko.fitconnect.api.domain.model.metadata.data.Data; +import de.fitko.fitconnect.api.domain.model.metadata.data.MimeType; import de.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; +import de.fitko.fitconnect.api.services.crypto.CryptoService; import de.fitko.fitconnect.client.factory.ClientFactory; +import de.fitko.fitconnect.client.model.AttachmentPayload; +import de.fitko.fitconnect.client.model.DataPayload; +import de.fitko.fitconnect.client.model.ReceivedSubmission; +import de.fitko.fitconnect.client.model.ServiceTypePayload; +import de.fitko.fitconnect.client.util.SubmissionUtil; +import de.fitko.fitconnect.core.crypto.HashService; +import de.fitko.fitconnect.core.crypto.JWECryptoService; +import org.apache.tika.mime.MimeTypes; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; 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.*; import java.util.stream.Collectors; @@ -33,16 +54,17 @@ class ClientIntegrationTest { final ApplicationConfig config = getConfigWithCredentialsFromGitlab(testEnv); final var sentSubmission = ClientFactory.senderClient(config) - .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) - .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") + .newSubmission() .withAttachment(new File("src/test/resources/attachment.txt")) .withJsonData("{ data: 'Beispiel Fachdaten' }") + .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) + .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") .submit(); assertTrue(sentSubmission.isPresent()); // When - final Optional<SubscriberClient.ReceivedSubmission> receivedSubmission = + final Optional<ReceivedSubmission> receivedSubmission = ClientFactory.subscriberClient(config) .requestSubmission(sentSubmission.get().getSubmissionId()); @@ -53,6 +75,79 @@ class ClientIntegrationTest { assertThat(new String(receivedSubmission.get().getAttachments().get(0).getDecryptedData()), is("Test attachment")); } + @Test + @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*") + void testSendAndConfirmCycleWithEncryptedData() throws ParseException, IOException { + + // Given + final UUID destinationId = UUID.fromString(System.getenv("TEST_DESTINATION_ID")); + final var testEnv = new Environment(authBaseUrl, submissionBaseUrl, true); + final ApplicationConfig config = getConfigWithCredentialsFromGitlab(testEnv); + final CryptoService cryptoService = new JWECryptoService(new HashService()); + final SenderClient.Start client = ClientFactory.senderClient(config); + + final Optional<String> publicKey = client.getPublicKey(destinationId); + assertTrue(publicKey.isPresent()); + + final RSAKey encryptionKey = RSAKey.parse(publicKey.get()); + + final String jsonData = "{ data: 'Beispiel Fachdaten' }"; + final File attachmentFile = new File("src/test/resources/attachment.txt"); + final String attachmentData = Files.readString(attachmentFile.toPath()); + + final String encryptedData = cryptoService.encryptString(encryptionKey, jsonData); + final String encryptedAttachment = cryptoService.encryptString(encryptionKey, attachmentData); + + final DataPayload dataPayload = DataPayload.builder() + .encryptedData(encryptedData) + .mimeType(MimeType.APPLICATION_JSON) + .schemaUri(URI.create("https://schema.fitko.de/fim/s00000000009_1.0.0.schema.json")) + .hashedData(cryptoService.hashBytes(jsonData.getBytes(StandardCharsets.UTF_8))) + .build(); + + final AttachmentPayload attachmentPayload = AttachmentPayload.builder() + .encryptedData(encryptedAttachment) + .file(attachmentFile) + .mimeType(MimeTypes.PLAIN_TEXT) + .attachmentId(UUID.randomUUID()) + .hashedData(cryptoService.hashBytes(attachmentData.getBytes(StandardCharsets.UTF_8))) + .build(); + + final ServiceTypePayload serviceTypePayload = ServiceTypePayload.builder() + .name("Test Service") + .leikaKey("urn:de:fim:leika:leistung:99400048079000") + .build(); + + final Data data = SubmissionUtil.buildData(dataPayload); + final PublicServiceType publicServiceType = SubmissionUtil.buildPublicServiceType(serviceTypePayload); + final List<Attachment> attachmentMetadata = SubmissionUtil.toAttachmentMetadata(List.of(attachmentPayload)); + final Metadata metadata = SubmissionUtil.buildMetadata(attachmentMetadata, data, publicServiceType); + + final String encryptedMetadata = cryptoService.encryptBytes(encryptionKey, new ObjectMapper().writeValueAsBytes(metadata)); + + // When + final var sentSubmission = client + .newSubmissionWithEncryptedData() + .withEncryptedAttachment(attachmentPayload.getAttachmentId(), attachmentPayload.getEncryptedData()) + .withEncryptedJsonData(encryptedData) + .withEncryptedMetadata(encryptedMetadata) + .withDestination(destinationId) + .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") + .submit(); + + assertTrue(sentSubmission.isPresent()); + + final Optional<ReceivedSubmission> receivedSubmission = + ClientFactory.subscriberClient(config) + .requestSubmission(sentSubmission.get().getSubmissionId()); + + // Then + assertTrue(receivedSubmission.isPresent()); + assertThat(receivedSubmission.get().getData(), is(jsonData)); + assertThat(receivedSubmission.get().getMimeType().toString(), is("application/json")); + assertThat(new String(receivedSubmission.get().getAttachments().get(0).getDecryptedData()), is("Test attachment")); + } + @Test @EnabledIfEnvironmentVariable(named = "TEST_DESTINATION_ID", matches = ".*") void testListSubmissions() { @@ -68,16 +163,16 @@ class ClientIntegrationTest { final String leikaKey = "urn:de:fim:leika:leistung:99400048079000"; final String serviceName = "Test Service"; - final var submissionOne = senderClient + final var submissionOne = senderClient.newSubmission() + .withJsonData("{ data: 'Beispiel Fachdaten 1' }") .withDestination(destinationId) .withServiceType(serviceName, leikaKey) - .withJsonData("{ data: 'Beispiel Fachdaten 1' }") .submit(); - final var submissionTwo = senderClient + final var submissionTwo = senderClient.newSubmission() + .withJsonData("{ data: 'Beispiel Fachdaten 2' }") .withDestination(destinationId) .withServiceType(serviceName, leikaKey) - .withJsonData("{ data: 'Beispiel Fachdaten 2' }") .submit(); assertTrue(submissionOne.isPresent()); @@ -107,11 +202,11 @@ class ClientIntegrationTest { final ApplicationConfig config = getConfigWithCredentialsFromGitlab("PROD", prodEnv); // When - final var sentSubmission = ClientFactory.senderClient(config) - .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) - .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") + final var sentSubmission = ClientFactory.senderClient(config).newSubmission() .withAttachment(new File("src/test/resources/attachment.txt")) .withJsonData("{ data: 'Beispiel Fachdaten' }") + .withDestination(UUID.fromString(System.getenv("TEST_DESTINATION_ID"))) + .withServiceType("Test Service", "urn:de:fim:leika:leistung:99400048079000") .submit(); // Then diff --git a/client/src/test/java/de/fitko/fitconnect/client/SenderClientTest.java b/client/src/test/java/de/fitko/fitconnect/client/SenderClientTest.java index f2937f047..2db080a57 100644 --- a/client/src/test/java/de/fitko/fitconnect/client/SenderClientTest.java +++ b/client/src/test/java/de/fitko/fitconnect/client/SenderClientTest.java @@ -32,7 +32,7 @@ import static org.mockito.Mockito.when; public class SenderClientTest { private Sender senderMock; - private SenderClient.WithDestination underTest; + private SenderClient.Start underTest; private final LogCaptor logs = new LogCaptor(); @@ -65,10 +65,11 @@ public class SenderClientTest { // When final var submission = underTest - .withDestination(destinationId) - .withServiceType("Service", "de:fitco:test:key") + .newSubmission() .withAttachment(new File("src/test/resources/attachment.txt")) .withJsonData("{}") + .withDestination(destinationId) + .withServiceType("Service", "de:fitco:test:key") .submit(); // Then @@ -98,11 +99,11 @@ public class SenderClientTest { when(senderMock.validateMetadata(any(), any())).thenReturn(ValidationResult.ok()); // When - final var submission = underTest - .withDestination(destinationId) - .withServiceType("Service", "de:fitco:test:key") + final var submission = underTest.newSubmission() .withAttachment(new File("src/test/resources/attachment.txt")) .withXmlData("<xml><test>data</test></xml>") + .withDestination(destinationId) + .withServiceType("Service", "de:fitco:test:key") .submit(); // Then @@ -120,11 +121,11 @@ public class SenderClientTest { final File file2 = new File("src/test/resources/pdf_attachment_with_no_extension"); // When - final var submission = underTest - .withDestination(destinationId) - .withServiceType("name", "test:key") + final var submission = underTest.newSubmission() .withAttachments(List.of(file1, file2)) .withJsonData("{}") + .withDestination(destinationId) + .withServiceType("name", "test:key") .submit(); // Then @@ -141,10 +142,10 @@ public class SenderClientTest { final UUID destinationId = setupTestMocks(); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData(null) .withDestination(destinationId) .withServiceType("name", "test:key") - .withJsonData(null) .submit(); // Then @@ -159,10 +160,10 @@ public class SenderClientTest { setupTestMocks(); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(null) .withServiceType("name", "test:key") - .withJsonData("{}") .submit(); // Then @@ -187,10 +188,10 @@ public class SenderClientTest { when(senderMock.getEncryptionKeyForDestination(any(), any())).thenReturn(publicKey); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("Service", "de:fitco:test:key") - .withJsonData("{}") .submit(); // Then @@ -205,10 +206,10 @@ public class SenderClientTest { final var destinationId = setupTestMocks(); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType(null, "leika:key") - .withJsonData("{}") .submit(); // Then @@ -217,16 +218,16 @@ public class SenderClientTest { } @Test - void testWithOptionalServiceTypeIdentifier() throws Exception { + void testWithMissingServiceTypeIdentifier() throws Exception { // Given final var destinationId = setupTestMocks(); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("Name", null) - .withJsonData("{}") .submit(); // Then @@ -241,10 +242,10 @@ public class SenderClientTest { final var destinationId = setupTestMocks(); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("Name", null, "leika:key") - .withJsonData("{}") .submit(); // Then @@ -258,11 +259,11 @@ public class SenderClientTest { final UUID destinationId = setupTestMocks(); // When - final var submission = underTest - .withDestination(destinationId) - .withServiceType("name", "test:key") + final var submission = underTest.newSubmission() .withAttachments(List.of()) .withJsonData("{}") + .withDestination(destinationId) + .withServiceType("name", "test:key") .submit(); // Then @@ -277,11 +278,11 @@ public class SenderClientTest { final UUID destinationId = setupTestMocks(); // When - final var submission = underTest - .withDestination(destinationId) - .withServiceType("name", "test:key") + final var submission = underTest.newSubmission() .withAttachment(null) .withJsonData("{}") + .withDestination(destinationId) + .withServiceType("name", "test:key") .submit(); // Then @@ -296,11 +297,11 @@ public class SenderClientTest { final UUID destinationId = setupTestMocks(); // When - final var submission = underTest - .withDestination(destinationId) - .withServiceType("name", "test:key") + final var submission = underTest.newSubmission() .withAttachment(new File("/non/existing/path")) .withJsonData("{}") + .withDestination(destinationId) + .withServiceType("name", "test:key") .submit(); // Then @@ -316,10 +317,10 @@ public class SenderClientTest { when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.error(new Exception())); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("name", "test:key") - .withJsonData("{}") .submit(); // Then @@ -335,10 +336,10 @@ public class SenderClientTest { when(senderMock.encryptBytes(any(),any())).thenThrow(new EncryptionException("Encryption failed")); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("name", "test:key") - .withJsonData("{}") .submit(); // Then @@ -354,10 +355,10 @@ public class SenderClientTest { when(senderMock.createSubmission(any())).thenThrow(new SubmissionNotCreatedException("Announcing submission failed")); // When - final var submission = underTest + final var submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("name", "test:key") - .withJsonData("{}") .submit(); // Then @@ -374,10 +375,10 @@ public class SenderClientTest { when(senderMock.getDestination(any())).thenThrow(new RestApiException("Loading destination failed")); // When - final Optional<SubmitSubmission> submission = underTest + final Optional<SubmitSubmission> submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("name", "test:key") - .withJsonData("{}") .submit(); assertTrue(submission.isEmpty()); @@ -393,16 +394,55 @@ public class SenderClientTest { when(senderMock.getEncryptionKeyForDestination(any(), any())).thenThrow(new KeyNotRetrievedException("Getting encryption key failed")); // When - final Optional<SubmitSubmission> submission = underTest + final Optional<SubmitSubmission> submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("name", "test:key") - .withJsonData("{}") .submit(); assertTrue(submission.isEmpty()); logs.assertContains("Getting encryption key for destination "+ destinationId+" failed"); } + @Test + void testSendSubmissionWithEncryptedData() throws Exception { + + // Given + final var destinationId = setupTestMocks(); + + // When + final Optional<SubmitSubmission> submission = underTest.newSubmissionWithEncryptedData() + .withEncryptedAttachments(Map.of(UUID.randomUUID(),"@tt@chm$nt")) + .withEncryptedJsonData("$ncrypt$d d@t@") + .withEncryptedMetadata("$ncrypt$d m$t@d@t@") + .withDestination(destinationId) + .withServiceType("name", "test:key") + .submit(); + + assertTrue(submission.isPresent()); + logs.assertContains("SUCCESSFULLY HANDED IN SUBMISSION"); + } + + @Test + void testPublicEncryptionKeyRetrieval() throws Exception { + + // Given + final var destinationId = UUID.randomUUID(); + final var destination = getDestination(destinationId); + + final RSAKey publicKey = generateRsaKey(4096).toPublicJWK(); + + when(senderMock.getDestination(any())).thenReturn(destination); + when(senderMock.validatePublicKey(any())).thenReturn(ValidationResult.ok()); + when(senderMock.getEncryptionKeyForDestination(any(), any())).thenReturn(publicKey); + + // When + final Optional<String> encryptionKey = underTest.getPublicKey(destinationId); + + assertTrue(encryptionKey.isPresent()); + assertThat(encryptionKey.get(), equalTo(publicKey.toJSONString())); + } + @Test void testFailOnInvalidMetadata() throws Exception { @@ -411,10 +451,10 @@ public class SenderClientTest { when(senderMock.validateMetadata(any(), any())).thenReturn(ValidationResult.error(new ValidationException("invalid metadata"))); // When - final Optional<SubmitSubmission> submission = underTest + final Optional<SubmitSubmission> submission = underTest.newSubmission() + .withJsonData("{}") .withDestination(destinationId) .withServiceType("name", "test:key") - .withJsonData("{}") .submit(); assertTrue(submission.isEmpty()); diff --git a/client/src/test/java/de/fitko/fitconnect/client/cmd/CommandLineClientTest.java b/client/src/test/java/de/fitko/fitconnect/client/cmd/CommandLineClientTest.java index 2643026bb..8e57ed88f 100644 --- a/client/src/test/java/de/fitko/fitconnect/client/cmd/CommandLineClientTest.java +++ b/client/src/test/java/de/fitko/fitconnect/client/cmd/CommandLineClientTest.java @@ -1,7 +1,5 @@ package de.fitko.fitconnect.client.cmd; -import ch.qos.logback.classic.spi.LoggingEvent; -import ch.qos.logback.core.AppenderBase; import de.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment; import de.fitko.fitconnect.api.domain.model.metadata.attachment.AttachmentWithData; import de.fitko.fitconnect.api.domain.model.metadata.data.MimeType; @@ -9,7 +7,7 @@ import de.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup; import de.fitko.fitconnect.api.domain.model.submission.SubmitSubmission; import de.fitko.fitconnect.client.SenderClient; import de.fitko.fitconnect.client.SubscriberClient; -import de.fitko.fitconnect.client.SubscriberClient.ReceivedSubmission; +import de.fitko.fitconnect.client.model.ReceivedSubmission; import de.fitko.fitconnect.client.testutil.LogCaptor; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -31,11 +29,11 @@ class CommandLineClientTest { private final LogCaptor logs = new LogCaptor(); - ByteArrayOutputStream sysoutCaptor = new ByteArrayOutputStream(); + private final ByteArrayOutputStream sysoutCaptor = new ByteArrayOutputStream(); - CommandLineClient underTest; - SenderClient.ClientBuilder senderClientMock; - SubscriberClient.ClientBuilder subscriberClientMock; + private CommandLineClient underTest; + private SenderClient.ClientBuilder senderClientMock; + private SubscriberClient.ClientBuilder subscriberClientMock; @BeforeEach void setUp() { @@ -120,6 +118,7 @@ class CommandLineClientTest { when(senderClientMock.withAttachments(any())).thenReturn(senderClientMock); when(senderClientMock.withJsonData(any())).thenReturn(senderClientMock); when(senderClientMock.submit()).thenReturn(Optional.of(expectedSubmission)); + when(senderClientMock.newSubmission()).thenReturn(senderClientMock); // When underTest.run("send", diff --git a/client/src/test/resources/test-config.yml b/client/src/test/resources/test-config.yml index f9b9ce501..b41a20611 100644 --- a/client/src/test/resources/test-config.yml +++ b/client/src/test/resources/test-config.yml @@ -3,15 +3,15 @@ httpProxyHost: "" httpProxyPort: 0 requestTimeoutInSeconds: 30 -# Name/key of the used environment +# Name/key of the used environment (from environments config below) activeEnvironment: dev -# Path that references the signing key file -privateSigningKeyPath: "client/src/test/java/resources/private_test_signing_key.json" - # Path that references the metadata schema metadataSchemaPath: "client/src/test/java/resources/metadata_schema.json" +# Path that references the signing key file +privateSigningKeyPath: "client/src/test/java/resources/private_test_signing_key.json" + senderConfig: clientId: "1" clientSecret: "123" diff --git a/core/src/main/java/de/fitko/fitconnect/core/auth/DefaultOAuthService.java b/core/src/main/java/de/fitko/fitconnect/core/auth/DefaultOAuthService.java index db38f08b0..7e2af3fab 100644 --- a/core/src/main/java/de/fitko/fitconnect/core/auth/DefaultOAuthService.java +++ b/core/src/main/java/de/fitko/fitconnect/core/auth/DefaultOAuthService.java @@ -44,7 +44,7 @@ public class DefaultOAuthService implements OAuthService { } @Override - public synchronized OAuthToken getCurrentToken() throws RestApiException { + public OAuthToken getCurrentToken() throws RestApiException { if (tokenExpired()) { LOGGER.info("Current token is expired, authenticating ..."); authenticate(); -- GitLab