Skip to content

Commit

Permalink
Merge pull request #12897 from SORMAS-Foundation/feature-12841-Add-va…
Browse files Browse the repository at this point in the history
…lidation-to-the-national-health-ID-field

#12841 Add validation to the national health ID field
  • Loading branch information
leventegal-she authored Jan 25, 2024
2 parents 9462aca + 11e56c8 commit a4cb6d6
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1397,6 +1397,7 @@ public interface Strings {
String messagePersonListAddedAsEventPerticipants = "messagePersonListAddedAsEventPerticipants";
String messagePersonMergedAddressDescription = "messagePersonMergedAddressDescription";
String messagePersonMergeNoEventParticipantRights = "messagePersonMergeNoEventParticipantRights";
String messagePersonNationalHealthIdInvalid = "messagePersonNationalHealthIdInvalid";
String messagePersonSaved = "messagePersonSaved";
String messagePersonSavedClassificationChanged = "messagePersonSavedClassificationChanged";
String messagePickEventParticipantsIncompleteSelection = "messagePickEventParticipantsIncompleteSelection";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public interface Validations {
String importUnexpectedError = "importUnexpectedError";
String importWrongDataTypeError = "importWrongDataTypeError";
String infrastructureDataLocked = "infrastructureDataLocked";
String invalidNationalHealthId = "invalidNationalHealthId";
String investigationStatusUnclassifiedCase = "investigationStatusUnclassifiedCase";
String jurisdictionChangeUserAssignment = "jurisdictionChangeUserAssignment";
String latitudeBetween = "latitudeBetween";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* SORMAS® - Surveillance Outbreak Response Management & Analysis System
* Copyright © 2016-2023 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
* Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package de.symeda.sormas.backend.externalemail.luxembourg;
package de.symeda.sormas.api.utils.luxembourg;

/**
* Apply Luhn algorithm to compute check digit
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* SORMAS® - Surveillance Outbreak Response Management & Analysis System
* Copyright © 2016-2023 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
* Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package de.symeda.sormas.backend.externalemail.luxembourg;
package de.symeda.sormas.api.utils.luxembourg;

/**
* Apply Verhoeff algorithm to compute check digit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* SORMAS® - Surveillance Outbreak Response Management & Analysis System
* Copyright © 2016-2024 Helmholtz-Zentrum für Infektionsforschung GmbH (HZI)
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package de.symeda.sormas.api.utils.luxembourg;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class LuxembourgNationalHealthIdValidator {

/**
* - AAAA = année de naissance
* - MM = mois de naissance
* - JJ = jour de naissance
* - XXX = numéro aléatoire unique par date de naissance
* - C1 = numéro de contrôle calculé sur AAAAMMJJXXX suivant l’algorithme LUHN 10
* - C2 = numéro de contrôle calculé sur AAAAMMJJXXX suivant l’algorithme VERHOEFF
*/
private static final Pattern NATIONAL_HEALTH_ID_PATTERN = Pattern.compile("(\\d{4})(\\d{2})(\\d{2})(\\d{3})(\\d)(\\d)");

public static boolean isValid(String nationalHealthId, Integer birthdateYYYY, Integer birthdateMM, Integer birthdateDD) {
if (nationalHealthId == null) {
return false;
}

Matcher patternMatcher = NATIONAL_HEALTH_ID_PATTERN.matcher(nationalHealthId);
if (!patternMatcher.matches()) {
return false;
}

String yyyy = patternMatcher.group(1);
String mm = patternMatcher.group(2);
String dd = patternMatcher.group(3);
String xxx = patternMatcher.group(4);
String c1 = patternMatcher.group(5);
String c2 = patternMatcher.group(6);

if (isNullOrEquals(birthdateYYYY, Integer.parseInt(yyyy))
&& isNullOrEquals(birthdateMM, Integer.parseInt(mm))
&& isNullOrEquals(birthdateDD, Integer.parseInt(dd))) {
String iNumber = yyyy + mm + dd + xxx;

if (CheckDigitLuhn.checkDigit(iNumber + c1) && CheckDigitVerhoeff.checkDigit(iNumber + c2)) {
return true;
}
}

return false;
}

private static boolean isNullOrEquals(Integer personBirthdateFieldValue, int yyyy) {
return personBirthdateFieldValue == null || personBirthdateFieldValue == yyyy;
}

}
1 change: 1 addition & 0 deletions sormas-api/src/main/resources/strings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,7 @@ messageExternalEmailNoAttachments=No attachments
messageCustomizableEnumValueSaved = Customizable enum value saved
messageExternalEmailAttachmentPassword=Please use this password to open the documents sent to you via email from SORMAS: %s
messageExternalEmailAttachmentNotAvailableInfo=Attaching documents is disabled because encryption would not be possible. To encrypt documents, the person needs to have either a national health ID specified, or a primary mobile phone number set with SMS sending set up on this system.
messagePersonNationalHealthIdInvalid=The entered national health ID does not seem to be correct
# Notifications
notificationCaseClassificationChanged = The classification of case %s has changed to %s.
notificationCaseInvestigationDone = The investigation of case %s has been done.
Expand Down
3 changes: 2 additions & 1 deletion sormas-api/src/main/resources/validations.properties
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,5 @@ customizableEnumValueAllowedCharacters = Value may only consist of uppercase let
customizableEnumValueEmptyTranslations = Please select languages and enter captions for all translations in the list.
customizableEnumValueDuplicateLanguage = Please only add one translation per language.
customizableEnumValueDuplicateValue = The value %s already exists for data type %s. Enum values have to be unique.
attachedDocumentNotRelatedToEntity=The attached document is not related to the entity.
attachedDocumentNotRelatedToEntity=The attached document is not related to the entity.
invalidNationalHealthId=This value does not seem to be a correct national health ID
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import de.symeda.sormas.api.travelentry.TravelEntryReferenceDto;
import de.symeda.sormas.api.utils.DataHelper.Pair;
import de.symeda.sormas.api.utils.ValidationRuntimeException;
import de.symeda.sormas.api.utils.luxembourg.LuxembourgNationalHealthIdValidator;
import de.symeda.sormas.backend.caze.CaseService;
import de.symeda.sormas.backend.common.ConfigFacadeEjb.ConfigFacadeEjbLocal;
import de.symeda.sormas.backend.common.messaging.EmailService;
Expand All @@ -75,7 +76,6 @@
import de.symeda.sormas.backend.document.DocumentService;
import de.symeda.sormas.backend.document.DocumentStorageService;
import de.symeda.sormas.backend.event.EventParticipantService;
import de.symeda.sormas.backend.externalemail.luxembourg.NationalHealthIdValidator;
import de.symeda.sormas.backend.manualmessagelog.ManualMessageLog;
import de.symeda.sormas.backend.manualmessagelog.ManualMessageLogService;
import de.symeda.sormas.backend.person.Person;
Expand Down Expand Up @@ -151,8 +151,8 @@ public void sendEmail(@Valid ExternalEmailOptionsDto options) throws DocumentTem

User currentUser = userService.getCurrentUser();
DocumentTemplateEntities documentEntities = templateEntitiesBuilder.resolveEntities(
new RootEntities().addReference(options.getRootEntityType(), options.getRootEntityReference())
.addEntity(RootEntityType.ROOT_USER, currentUser));
new RootEntities().addReference(options.getRootEntityType(), options.getRootEntityReference())
.addEntity(RootEntityType.ROOT_USER, currentUser));

PersonReferenceDto personRef = (PersonReferenceDto) documentEntities.getEntity(RootEntityType.ROOT_PERSON);
Person person = personService.getByReferenceDto(personRef);
Expand All @@ -176,18 +176,18 @@ public void sendEmail(@Valid ExternalEmailOptionsDto options) throws DocumentTem
}

String generatedText =
documentTemplateFacade.generateDocumentTxtFromEntities(options.getDocumentWorkflow(), options.getTemplateName(), documentEntities, null);
documentTemplateFacade.generateDocumentTxtFromEntities(options.getDocumentWorkflow(), options.getTemplateName(), documentEntities, null);
EmailTemplateTexts emailTexts = splitTemplateContent(generatedText);

try {
emailService.sendEmail(options.getRecipientEmail(), emailTexts.getSubject(), emailTexts.getContent(), attachments);

if (passwordType == PasswordType.RANDOM) {
messagingService.sendManualMessage(
person,
null,
String.format(I18nProperties.getString(Strings.messageExternalEmailAttachmentPassword), password),
MessageType.SMS);
person,
null,
String.format(I18nProperties.getString(Strings.messageExternalEmailAttachmentPassword), password),
MessageType.SMS);
}
} catch (MessagingException | NotificationDeliveryFailedException e) {
logger.error("Error sending email", e);
Expand All @@ -201,9 +201,9 @@ public void sendEmail(@Valid ExternalEmailOptionsDto options) throws DocumentTem
private static void validateAttachedDocuments(List<Document> sormasDocuments, ExternalEmailOptionsDto options) {
DocumentRelatedEntityType documentRelatedEntityType = DOCUMENT_WORKFLOW_DOCUMENT_RELATION_MAPPING.get(options.getDocumentWorkflow());
if (sormasDocuments.stream()
.anyMatch(
d -> d.getRelatedEntityType() != documentRelatedEntityType
&& !Objects.equals(d.getRelatedEntityUuid(), options.getRootEntityReference().getUuid()))) {
.anyMatch(
d -> d.getRelatedEntityType() != documentRelatedEntityType
&& !Objects.equals(d.getRelatedEntityUuid(), options.getRootEntityReference().getUuid()))) {
throw new ValidationRuntimeException(I18nProperties.getValidationError(Validations.attachedDocumentNotRelatedToEntity));
}
}
Expand Down Expand Up @@ -238,30 +238,30 @@ private Pair<String, PasswordType> getPassword(Person person) throws ExternalEma
PasswordType passwordType = getApplicablePasswordType(person);

switch (passwordType) {
case HEALTH_ID:
return new Pair<>(person.getNationalHealthId(), passwordType);
case RANDOM:
return new Pair<>(generateRandomPassword(), passwordType);
case NONE:
default:
throw new ExternalEmailException(I18nProperties.getString(Strings.errorExternalEmailAttachmentCannotEncrypt));
case HEALTH_ID:
return new Pair<>(person.getNationalHealthId(), passwordType);
case RANDOM:
return new Pair<>(generateRandomPassword(), passwordType);
case NONE:
default:
throw new ExternalEmailException(I18nProperties.getString(Strings.errorExternalEmailAttachmentCannotEncrypt));
}
}

private static String generateRandomPassword() {
return new RandomStringGenerator.Builder().withinRange(
new char[]{
'a',
'z'},
new char[]{
'A',
'Z'},
new char[]{
'2',
'9'})
.filteredBy(codePoint -> !"lIO".contains(String.valueOf((char) codePoint)))
.build()
.generate(ATTACHMENT_PASSWORD_LENGTH);
new char[] {
'a',
'z' },
new char[] {
'A',
'Z' },
new char[] {
'2',
'9' })
.filteredBy(codePoint -> !"lIO".contains(String.valueOf((char) codePoint)))
.build()
.generate(ATTACHMENT_PASSWORD_LENGTH);
}

private static void validateOptions(ExternalEmailOptionsDto options) {
Expand All @@ -272,10 +272,10 @@ private static void validateOptions(ExternalEmailOptionsDto options) {
}

private ManualMessageLog createMessageLog(
ExternalEmailOptionsDto options,
PersonReferenceDto personRef,
User currentUser,
List<Document> attachedDocuments) {
ExternalEmailOptionsDto options,
PersonReferenceDto personRef,
User currentUser,
List<Document> attachedDocuments) {
ManualMessageLog log = new ManualMessageLog();

log.setMessageType(MessageType.EMAIL);
Expand All @@ -290,10 +290,10 @@ private ManualMessageLog createMessageLog(
log.setCaze(caseService.getByReferenceDto(getRootEntityReference(options, RootEntityType.ROOT_CASE, CaseReferenceDto.class)));
log.setContact(contactService.getByReferenceDto(getRootEntityReference(options, RootEntityType.ROOT_CONTACT, ContactReferenceDto.class)));
log.setEventParticipant(
eventParticipantService
.getByReferenceDto(getRootEntityReference(options, RootEntityType.ROOT_EVENT_PARTICIPANT, EventParticipantReferenceDto.class)));
eventParticipantService
.getByReferenceDto(getRootEntityReference(options, RootEntityType.ROOT_EVENT_PARTICIPANT, EventParticipantReferenceDto.class)));
log.setTravelEntry(
travelEntryService.getByReferenceDto(getRootEntityReference(options, RootEntityType.ROOT_TRAVEL_ENTRY, TravelEntryReferenceDto.class)));
travelEntryService.getByReferenceDto(getRootEntityReference(options, RootEntityType.ROOT_TRAVEL_ENTRY, TravelEntryReferenceDto.class)));

return log;
}
Expand All @@ -312,7 +312,7 @@ private static boolean isValidLuxembourgNationalHealthId(String nationalHealthId
return false;
}

return NationalHealthIdValidator.isValid(nationalHealthId, person);
return LuxembourgNationalHealthIdValidator.isValid(nationalHealthId, person.getBirthdateYYYY(), person.getBirthdateMM(), person.getBirthdateDD());
}

@Stateless
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package de.symeda.sormas.backend.externalemail;

import static de.symeda.sormas.backend.docgeneration.TemplateTestUtil.updateLineSeparatorsBasedOnOS;
import static de.symeda.sormas.backend.externalemail.luxembourg.NationalHealthIdValidatorTest.VALID_LU_NATIONAL_HEALTH_ID;
import static de.symeda.sormas.backend.util.luxembourg.LuxembourgNationalHealthIdValidatorTest.VALID_LU_NATIONAL_HEALTH_ID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasItem;
Expand Down
Loading

0 comments on commit a4cb6d6

Please sign in to comment.