From 6cd9ffdf713473869ffe73cf402a755bd9cc9030 Mon Sep 17 00:00:00 2001
From: Martin Vogel <martin.vogel@sinc.de>
Date: Wed, 22 Jun 2022 18:40:45 +0200
Subject: [PATCH] #414 Extend subscriber with validation methods and add
 submission retrieval workflow to the client

---
 .../fitconnect/api/services/Subscriber.java   |  91 ++++++------
 .../fitconnect/client/SubscriberClient.java   | 138 +++++++++++-------
 .../core}/SubmissionSubscriber.java           |  52 ++++---
 3 files changed, 160 insertions(+), 121 deletions(-)
 rename {impl/src/main/java/de/fitko/fitconnect/impl => core/src/main/java/de/fitko/fitconnect/core}/SubmissionSubscriber.java (56%)

diff --git a/api/src/main/java/de/fitko/fitconnect/api/services/Subscriber.java b/api/src/main/java/de/fitko/fitconnect/api/services/Subscriber.java
index d2d175dc4..59ed0ea8a 100644
--- a/api/src/main/java/de/fitko/fitconnect/api/services/Subscriber.java
+++ b/api/src/main/java/de/fitko/fitconnect/api/services/Subscriber.java
@@ -5,88 +5,93 @@ import de.fitko.fitconnect.api.domain.auth.OAuthToken;
 import de.fitko.fitconnect.api.domain.model.metadata.Metadata;
 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.event.SecurityEventToken;
-import de.fitko.fitconnect.api.domain.model.submission.SubmissionRequest;
+import de.fitko.fitconnect.api.domain.model.submission.Submission;
+import de.fitko.fitconnect.api.domain.model.submission.SubmissionForPickup;
+import de.fitko.fitconnect.api.domain.model.submission.SubmissionSubmit;
 import de.fitko.fitconnect.api.domain.validation.ValidationResult;
 
 import java.util.List;
-import java.util.Optional;
+import java.util.UUID;
 
 /**
  * A technical system that accepts submissions on the administration side.
  *
- *  @see <a href="https://docs.fitko.de/fit-connect/docs/receiving/overview">Receiving Submissions</a>
+ * @see <a href="https://docs.fitko.de/fit-connect/docs/receiving/overview">Receiving Submissions</a>
  */
 public interface Subscriber {
 
     /**
      * Authenticates the {@link Subscriber} against the FitConnect-Auth-API and retrieves an {@link OAuthToken}.
      *
-     * @param clientId a unique client identifier
+     * @param clientId     a unique client identifier
      * @param clientSecret the applications secret key
-     * @param scope 1 or more client scopes that determine if a submission is accepted by the client
+     * @param scope        1 or more client scopes that determine if a submission is accepted by the client
      * @return {@link OAuthToken}, is empty if an error occurred
      */
-    Optional<OAuthToken> retrieveOAuthToken(final String clientId, final String clientSecret, final String... scope);
+    OAuthToken retrieveOAuthToken(final String clientId, final String clientSecret, final String... scope);
 
     /**
-     * Decrypts the JWE-encrypted submission data payload (json or xml).
+     * Decrypts JWE-encrypted string data.
      *
-     * @param privateKey the private key to decrypt the {@link Data} payload
-     * @param encryptedData the {@link Data} that is decrypted
-     * @return the decrypted {@link Data}, is empty if en error occurred
+     * @param privateKey       the private key to decrypt the JWE-encrypted string
+     * @param encryptedContent the content that is decrypted
+     * @return the decrypted content as byte[]
      */
-    Optional<Data> decryptSubmissionData(final RSAKey privateKey, final Data encryptedData);
+    byte[] decryptStringContent(final RSAKey privateKey, final String encryptedContent);
 
     /**
-     * Decrypts the JWE-encrypted submission attachment binary data.
+     * Polls for available {@link SubmissionSubmit}s on the given destinationId
      *
-     * @param privateKey the private key to decrypt the {@link Attachment}
-     * @param encryptedAttachment the JWE-encrypted {@link Attachment} that should be decrypted
-     * @return the decrypted {@link Attachment}, is empty if en error occurred
+     * @param destinationId restricts the query to a specific destination
+     * @param limit         number of submissions in result (max. is 500)
+     * @param offset        position in the dataset
+     * @return list of found submissions
      */
-    Optional<Attachment> decryptAttachment(final RSAKey privateKey, final Attachment encryptedAttachment);
+    List<SubmissionForPickup> pollAvailableSubmissions(UUID destinationId, int limit, int offset);
 
     /**
-     * Validates the {@link Metadata} structure against a given JSON-schema to ensure its correctness.
+     * Gets a specific {@link SubmissionSubmit}.
      *
-     * @param metadata the {@link Metadata} object that is validated
-     * @param jsonSchema the schema in JSON-format that the given {@link Metadata} structure is compared with
-     * @return a {@link ValidationResult}, contains error if the {@link Metadata} doesn't match the schema
+     * @param submissionId the unique identifier of a {@link SubmissionSubmit}
+     * @return the optional submission, is empty if none was found
      */
-    ValidationResult validateMetadataSchema(final Metadata metadata, final String jsonSchema);
+    Submission getSubmission(UUID submissionId);
 
     /**
-     * Validates the hash-values of the {@link Metadata}s {@link Data} and {@link Attachment} in order to guarantee
-     * the integrity of the transmitted information.
-     *
-     * @param metadata the {@link Metadata} object including the hashes that where created by the {@link Sender}
-     * @return a {@link ValidationResult}, that contains errors if the hash-values of the transmitted data are not valid
+     * @param submissionId
+     * @param announcedAttachments
+     * @return
      */
-    ValidationResult validateMetadataHashValues(final Metadata metadata);
+    List<Attachment> fetchAttachments(UUID submissionId, List<UUID> announcedAttachments);
 
     /**
-     * Polls for available {@link SubmissionRequest}s on the given destinationId
-     *
-     * @param destinationId restricts the query to a specific destination
-     * @param limit number of submissions in result (max. is 500)
-     * @param offset position in the dataset
-     *
-     * @return list of found submissions
+     * @param caseId
+     * @return
      */
-    List<SubmissionRequest> pollAvailableSubmissions(String destinationId, int limit, int offset);
+    ValidationResult validateEventLog(UUID caseId);
 
     /**
-     * Gets a specific {@link SubmissionRequest}.
-     *
-     * @param submissionId the unique identifier of a {@link SubmissionRequest}
-     *
-     * @return the optional submission, is empty if none was found
+     * Validates the {@link Metadata} structure against a given JSON-schema to ensure its correctness.
      *
+     * @param metadata the {@link Metadata} object that is validated
+     * @return a {@link ValidationResult}, contains error if the {@link Metadata} doesn't match the schema
      */
-    Optional<SubmissionRequest> getSubmission(String submissionId);
+    ValidationResult validateMetadata(Metadata metadata);
 
+    /**
+     * @param data
+     * @return
+     */
+    ValidationResult validateData(Data data);
 
-    Optional<SecurityEventToken> createSecurityEventToken(Data data, Attachment attachment, RSAKey privateKey);
+    /**
+     * @param attachments
+     * @return
+     */
+    ValidationResult validateAttachments(List<Attachment> attachments);
 
+    /**
+     *
+     */
+    void confirmValidSubmission();
 }
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 62e74c2a9..144e0bc24 100644
--- a/client/src/main/java/de/fitko/fitconnect/client/SubscriberClient.java
+++ b/client/src/main/java/de/fitko/fitconnect/client/SubscriberClient.java
@@ -1,114 +1,142 @@
 package de.fitko.fitconnect.client;
 
+import com.nimbusds.jose.jwk.RSAKey;
 import de.fitko.fitconnect.api.domain.auth.OAuthToken;
 import de.fitko.fitconnect.api.domain.model.metadata.Metadata;
 import de.fitko.fitconnect.api.domain.model.metadata.attachment.Attachment;
-import de.fitko.fitconnect.api.domain.model.submission.SubmissionRequest;
+import de.fitko.fitconnect.api.domain.model.metadata.data.Data;
+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.ClientNotAuthenticatedException;
+import de.fitko.fitconnect.api.exceptions.DecryptionException;
+import de.fitko.fitconnect.api.exceptions.ValidationException;
 import de.fitko.fitconnect.api.services.Subscriber;
 
+import java.text.ParseException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 /**
  * A fluent client for handing in a subscription for a {@link Subscriber}
  */
-public class SubscriberClient extends Client {
+public class SubscriberClient {
 
-    private SubscriberClient() {}
-
-     public static Authenticate builder() {
-         final Subscriber subscriber = getService(Subscriber.class);
-         return new ClientBuilder(subscriber);
+    private SubscriberClient() {
     }
 
-    public interface Authenticate {
-        PollSubmissions authenticate(String clientId, String secret, String... scope);
+    public static RequestSubmission builder(final Subscriber subscriber, final String clientId, final String clientSecret, final String privateKey) {
+        return new ClientBuilder(subscriber, clientId, clientSecret, privateKey);
     }
 
-    public interface PollSubmissions {
-
-        List<SubmissionRequest> getAvailableSubmissions(String destinationId);
-        RequestMetadata requestSubmission(SubmissionRequest submission);
-    }
+    public interface RequestSubmission {
+        List<SubmissionForPickup> getAvailableSubmissions(UUID destinationId, int offset, int limit);
 
-    public interface RequestMetadata {
-        RequestAttachments requestMetadata();
-    }
+        List<SubmissionForPickup> getAvailableSubmissions(UUID destinationId);
 
-    public interface  RequestAttachments {
-        ConfirmSubmission requestAttachments();
+        GetData requestSubmission(UUID submissionId);
     }
 
-    public interface ConfirmSubmission {
-        AccessData confirmSubmission();
-    }
-
-    public interface AccessData {
-        SubmissionRequest getSubmission();
+    public interface GetData {
         List<Attachment> getAttachments();
+
         Metadata getMetadata();
+
+        Data getData();
     }
 
+    public static class ClientBuilder implements RequestSubmission, GetData {
 
-    public static class ClientBuilder implements Authenticate, PollSubmissions, RequestMetadata, RequestAttachments, ConfirmSubmission, AccessData {
+        private static final int DEFAULT_SUBMISSION_LIMIT = 100;
 
         private final Subscriber subscriber;
+        private final String clientId;
+        private final String secret;
+        private final String privateKey;
 
-        private Optional<SubmissionRequest> submission = Optional.empty();
-        private Optional<Metadata> metadata = Optional.empty();
-        private List<Attachment> attachments = Collections.emptyList();
-        private Optional<OAuthToken> token;
-
-        private ClientBuilder(Subscriber subscriber) {
+        public ClientBuilder(final Subscriber subscriber, final String clientId, final String clientSecret, final String privateKey) {
             this.subscriber = subscriber;
+            this.clientId = clientId;
+            this.secret = clientSecret;
+            this.privateKey = privateKey;
+            authenticate();
         }
 
         @Override
-        public PollSubmissions authenticate(String clientId, String secret, String... scope) {
-            this.token = subscriber.retrieveOAuthToken(clientId, secret, scope);
-            return this;
+        public List<SubmissionForPickup> getAvailableSubmissions(final UUID destinationId) {
+            return getAvailableSubmissions(destinationId, 0, DEFAULT_SUBMISSION_LIMIT);
         }
 
         @Override
-        public List<SubmissionRequest> getAvailableSubmissions(String destinationId) {
-            throw new UnsupportedOperationException("not yet implemented");
+        public List<SubmissionForPickup> getAvailableSubmissions(final UUID destinationId, final int offset, final int limit) {
+            return subscriber.pollAvailableSubmissions(destinationId, offset, limit);
         }
 
         @Override
-        public RequestMetadata requestSubmission(SubmissionRequest submission) {
-            throw new UnsupportedOperationException("not yet implemented");
-        }
+        public GetData requestSubmission(final UUID submissionId) {
 
+            final Submission submission = subscriber.getSubmission(submissionId);
+            final List<Attachment> attachments = subscriber.fetchAttachments(submissionId, submission.getAttachments());
+            final String encryptedData = submission.getEncryptedData();
+            final String encryptedMetadata = submission.getEncryptedMetadata();
 
-        @Override
-        public RequestAttachments requestMetadata() {
-            throw new UnsupportedOperationException("not yet implemented");
+            final RSAKey decryptionKey = parseDecryptionKey(privateKey);
+
+            final byte[] decryptedSubmissionData = subscriber.decryptStringContent(decryptionKey, encryptedData);
+            final byte[] decryptedMetadata = subscriber.decryptStringContent(decryptionKey, encryptedMetadata);
+            // TODO generate objects metadata and data from byte[]
+
+            final List<ValidationResult> validationsResults = Collections.emptyList();
+            validationsResults.add(subscriber.validateEventLog(submission.getCaseId()));
+            validationsResults.add(subscriber.validateMetadata(getMetadata()));
+            validationsResults.add(subscriber.validateData(getData()));
+            validationsResults.add(subscriber.validateAttachments(getAttachments()));
+
+            final Optional<ValidationResult> validationFailed = findFailedValidation(validationsResults);
+
+            if (validationFailed.isEmpty()) {
+                subscriber.confirmValidSubmission();
+                return this;
+            }
+
+            throw new ValidationException("Validation has errors, submission is not valid");
         }
 
         @Override
-        public ConfirmSubmission requestAttachments() {
-            throw new UnsupportedOperationException("not yet implemented");
+        public List<Attachment> getAttachments() {
+            return null;
         }
 
         @Override
-        public AccessData confirmSubmission() {
-            throw new UnsupportedOperationException("not yet implemented");
+        public Metadata getMetadata() {
+            return null;
         }
 
         @Override
-        public SubmissionRequest getSubmission() {
-            return this.submission.orElseThrow();
+        public Data getData() {
+            return null;
         }
 
-        @Override
-        public List<Attachment> getAttachments() {
-            return this.attachments;
+        private RSAKey parseDecryptionKey(final String privateKey) {
+            try {
+                return RSAKey.parse(privateKey);
+            } catch (final ParseException e) {
+                throw new DecryptionException("Key could not be parsed", e);
+            }
         }
 
-        @Override
-        public Metadata getMetadata() {
-            return this.metadata.orElseThrow();
+        private Optional<ValidationResult> findFailedValidation(final List<ValidationResult> validationsResults) {
+            return validationsResults.stream().filter(result -> result.hasError()).findFirst();
         }
+
+        private void authenticate() {
+            final OAuthToken oAuthToken = subscriber.retrieveOAuthToken(clientId, secret);
+            if (oAuthToken == null) {
+                throw new ClientNotAuthenticatedException("Client is not authenticated, please authenticate first");
+            }
+        }
+
     }
 }
\ No newline at end of file
diff --git a/impl/src/main/java/de/fitko/fitconnect/impl/SubmissionSubscriber.java b/core/src/main/java/de/fitko/fitconnect/core/SubmissionSubscriber.java
similarity index 56%
rename from impl/src/main/java/de/fitko/fitconnect/impl/SubmissionSubscriber.java
rename to core/src/main/java/de/fitko/fitconnect/core/SubmissionSubscriber.java
index 0a672df5a..b84b15bfe 100644
--- a/impl/src/main/java/de/fitko/fitconnect/impl/SubmissionSubscriber.java
+++ b/core/src/main/java/de/fitko/fitconnect/core/SubmissionSubscriber.java
@@ -1,4 +1,4 @@
-package de.fitko.fitconnect.impl;
+package de.fitko.fitconnect.core;
 
 import com.google.inject.Inject;
 import com.nimbusds.jose.jwk.RSAKey;
@@ -6,10 +6,9 @@ import de.fitko.fitconnect.api.domain.auth.OAuthToken;
 import de.fitko.fitconnect.api.domain.model.metadata.Metadata;
 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.event.SecurityEventToken;
-import de.fitko.fitconnect.api.domain.model.submission.SubmissionRequest;
+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.internal.AuthenticationException;
 import de.fitko.fitconnect.api.services.Subscriber;
 import de.fitko.fitconnect.api.services.auth.OAuthService;
 import de.fitko.fitconnect.api.services.crypto.CryptoService;
@@ -18,8 +17,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.List;
-import java.util.Optional;
-
+import java.util.UUID;
 
 public class SubmissionSubscriber implements Subscriber {
 
@@ -32,54 +30,62 @@ public class SubmissionSubscriber implements Subscriber {
     @Inject
     public SubmissionSubscriber(final OAuthService authService,
                                 final CryptoService cryptoService,
-                                final MetadataValidator metadataValidator){
+                                final MetadataValidator metadataValidator) {
         this.authService = authService;
         this.cryptoService = cryptoService;
         this.metadataValidator = metadataValidator;
     }
 
     @Override
-    public Optional<OAuthToken> retrieveOAuthToken(String clientId, String clientSecret, String... scope) {
-        try {
-            return Optional.of(authService.authenticate(clientId, clientSecret, scope));
-        } catch (AuthenticationException e) {
-            logger.error("Retrieving the OAuth token failed: ", e.getMessage());
-            return Optional.empty();
-        }
+    public OAuthToken retrieveOAuthToken(final String clientId, final String clientSecret, final String... scope) {
+        final OAuthToken token = authService.authenticate(clientId, clientSecret, scope);
+        logger.debug("Successfully retrieved OAuth token: {}", token.getAccessToken());
+        return token;
+    }
+
+    @Override
+    public byte[] decryptStringContent(final RSAKey privateKey, final String encryptedContent) {
+        return cryptoService.decryptBytes(privateKey, encryptedContent);
     }
 
     @Override
-    public Optional<Data> decryptSubmissionData(RSAKey privateKey, Data encryptedData) {
+    public List<SubmissionForPickup> pollAvailableSubmissions(final UUID destinationId, final int limit, final int offset) {
         throw new UnsupportedOperationException("not yet implemented");
     }
 
     @Override
-    public Optional<Attachment> decryptAttachment(RSAKey privateKey, Attachment encryptedAttachment) {
+    public Submission getSubmission(final UUID submissionId) {
         throw new UnsupportedOperationException("not yet implemented");
     }
 
     @Override
-    public ValidationResult validateMetadataSchema(Metadata metadata, String jsonSchema) {
-        return metadataValidator.validateMetadataSchema(metadata, jsonSchema);
+    public List<Attachment> fetchAttachments(final UUID submissionId, final List<UUID> announcedAttachments) {
+        throw new UnsupportedOperationException("not yet implemented");
     }
 
     @Override
-    public ValidationResult validateMetadataHashValues(Metadata metadata) {
-        return metadataValidator.validateMetadataHashValues(metadata);
+    public ValidationResult validateEventLog(final UUID caseId) {
+        throw new UnsupportedOperationException("not yet implemented");
     }
 
     @Override
-    public List<SubmissionRequest> pollAvailableSubmissions(String destinationId, int limit, int offset) {
+    public ValidationResult validateMetadata(final Metadata metadata) {
         throw new UnsupportedOperationException("not yet implemented");
     }
 
     @Override
-    public Optional<SubmissionRequest> getSubmission(String submissionId) {
+    public ValidationResult validateData(final Data data) {
         throw new UnsupportedOperationException("not yet implemented");
     }
 
     @Override
-    public Optional<SecurityEventToken> createSecurityEventToken(Data data, Attachment attachment, RSAKey privateKey) {
+    public ValidationResult validateAttachments(final List<Attachment> attachments) {
         throw new UnsupportedOperationException("not yet implemented");
     }
+
+    @Override
+    public void confirmValidSubmission() {
+        throw new UnsupportedOperationException("not yet implemented");
+    }
+
 }
-- 
GitLab