Repository: corona-warn-app/cwa-server
File: services/submission/src/main/java/app/coronawarn/server/services/submission/validation/ValidSubmissionPayload.java (Java)

Code before:
     *   <li>Either a value of accepted Transmission Risk Level or an accepted Days Since Onset Of Symptoms
     *       must be present. If one value is missing, the other one can be derived
     *       (see {@link DiagnosisKeyNormalizer}</li>
     * </ul>
     */
    @Override
    public boolean isValid(SubmissionPayload submissionPayload, ConstraintValidatorContext validatorContext) {
      List<TemporaryExposureKey> exposureKeys = submissionPayload.getKeysList();
      validatorContext.disableDefaultConstraintViolation();

      boolean isValidPayload = checkStartIntervalNumberIsAtMidNight(exposureKeys, validatorContext)
          && checkKeyCollectionSize(exposureKeys, validatorContext)
          && checkOriginCountryIsValid(submissionPayload, validatorContext)
          && checkVisitedCountriesAreValid(submissionPayload, validatorContext)
          && checkRequiredFieldsNotMissing(exposureKeys, validatorContext)
          && checkTransmissionRiskLevelIsAcceptable(exposureKeys, validatorContext)
          && checkDaysSinceOnsetOfSymptomsIsInRange(exposureKeys, validatorContext);

      if (!isValidPayload) {
        String payloadString = generatePrintablePayload(submissionPayload);
        logger.error("Errors caused by invalid payload {}", payloadString);
      }
      return isValidPayload;
    }

    private String generatePrintablePayload(SubmissionPayload submissionPayload) {
      StringBuilder stringBuilder = new StringBuilder();

      stringBuilder
          .append(" payload origin: ").append(submissionPayload.getOrigin())
          .append(" visited_countries: ").append(submissionPayload.getVisitedCountriesList())
          .append(" consent_to_federation: ").append(submissionPayload.getConsentToFederation())
          .append(" with padding_size: ").append(submissionPayload.getRequestPadding().size());
      stringBuilder.append(" keys: ");
      for (TemporaryExposureKey key : submissionPayload.getKeysList()) {
        stringBuilder
            .append(" {")
            .append(" key_data: HIDDEN")
            .append(" transmission_risk_level: ").append(key.getTransmissionRiskLevel())
            .append(" rolling_start_interval_number: ").append(key.getRollingStartIntervalNumber())
            .append(" report_type: ").append(key.getReportType())
            .append(" days_since_onset_of_symptoms: ").append(key.getDaysSinceOnsetOfSymptoms())
            .append(" }");

      }
      return stringBuilder.toString();
    }

    private void addViolation(ConstraintValidatorContext validatorContext, String message) {
      validatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }

Review comment (security):
The solution is good enough if we need it quickly, but one suggestion from my side is to extract this sort of "produce printable object" logic outside the validator in a dedicated logging utility class. This would encapsulate such printing responsabilities and can be further extended with other objects.  You can then have unit tests around how the printed object should look like (i.e. not contain sensitive fields maybe)

Code after:
     *   <li>Either a value of accepted Transmission Risk Level or an accepted Days Since Onset Of Symptoms
     *       must be present. If one value is missing, the other one can be derived
     *       (see {@link DiagnosisKeyNormalizer}</li>
     * </ul>
     */
    @Override
    public boolean isValid(SubmissionPayload submissionPayload, ConstraintValidatorContext validatorContext) {
      List<TemporaryExposureKey> exposureKeys = submissionPayload.getKeysList();
      validatorContext.disableDefaultConstraintViolation();

      boolean isValidPayload = checkStartIntervalNumberIsAtMidNight(exposureKeys, validatorContext)
          && checkKeyCollectionSize(exposureKeys, validatorContext)
          && checkOriginCountryIsValid(submissionPayload, validatorContext)
          && checkVisitedCountriesAreValid(submissionPayload, validatorContext)
          && checkRequiredFieldsNotMissing(exposureKeys, validatorContext)
          && checkTransmissionRiskLevelIsAcceptable(exposureKeys, validatorContext)
          && checkDaysSinceOnsetOfSymptomsIsInRange(exposureKeys, validatorContext);

      if (!isValidPayload) {
        PrintableSubmissionPayload printableSubmissionPayload = new PrintableSubmissionPayload(submissionPayload);
        logger.error("Errors caused by invalid payload {}", printableSubmissionPayload);
      }
      return isValidPayload;
    }

    private void addViolation(ConstraintValidatorContext validatorContext, String message) {
      validatorContext.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }

    private boolean checkKeyCollectionSize(List<TemporaryExposureKey> exposureKeys,
        ConstraintValidatorContext validatorContext) {
      if (exposureKeys.isEmpty() || exposureKeys.size() > maxNumberOfKeys) {
        addViolation(validatorContext, String.format(
            "Number of keys must be between 1 and %s, but is %s.", maxNumberOfKeys, exposureKeys.size()));
        return false;
      }
      return true;
    }

    private boolean checkStartIntervalNumberIsAtMidNight(List<TemporaryExposureKey> exposureKeys,
        ConstraintValidatorContext validatorContext) {
      boolean isNotMidNight00Utc = exposureKeys.stream()
          .anyMatch(exposureKey -> exposureKey.getRollingStartIntervalNumber() % maxRollingPeriod > 0);

      if (isNotMidNight00Utc) {
        addViolation(validatorContext, "Start Interval Number must be at midnight ( 00:00 UTC )");
        return false;
      }

      return true;
    }