From 586965a4fb45b25aa586580e017a7771b11406a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 03:45:07 +0000 Subject: [PATCH 01/52] Bump org.springframework.boot:spring-boot-starter-parent Bumps [org.springframework.boot:spring-boot-starter-parent](https://github.com/spring-projects/spring-boot) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.1.3...v3.1.4) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-parent dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e07376d54..e88a9dcf0 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.3 + 3.1.4 From 0a22455c34cdf31f9e779dbd68c50f9d0824287d Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Wed, 27 Sep 2023 10:23:20 +0200 Subject: [PATCH 02/52] Fix #878: Set develop version to 1.6.0-SNAPSHOT --- enrollment-server-api-model/pom.xml | 2 +- enrollment-server-onboarding-adapter-mock/pom.xml | 2 +- enrollment-server-onboarding-api-model/pom.xml | 2 +- enrollment-server-onboarding-common/pom.xml | 2 +- enrollment-server-onboarding-domain-model/pom.xml | 2 +- enrollment-server-onboarding/pom.xml | 2 +- enrollment-server/pom.xml | 2 +- mtoken-model/pom.xml | 2 +- pom.xml | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/enrollment-server-api-model/pom.xml b/enrollment-server-api-model/pom.xml index 96073dbc9..a798a3719 100644 --- a/enrollment-server-api-model/pom.xml +++ b/enrollment-server-api-model/pom.xml @@ -30,7 +30,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT diff --git a/enrollment-server-onboarding-adapter-mock/pom.xml b/enrollment-server-onboarding-adapter-mock/pom.xml index 3ba8def77..99f5897a6 100644 --- a/enrollment-server-onboarding-adapter-mock/pom.xml +++ b/enrollment-server-onboarding-adapter-mock/pom.xml @@ -24,7 +24,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT enrollment-server-onboarding-adapter-mock diff --git a/enrollment-server-onboarding-api-model/pom.xml b/enrollment-server-onboarding-api-model/pom.xml index 51eb931a8..b22e220c9 100644 --- a/enrollment-server-onboarding-api-model/pom.xml +++ b/enrollment-server-onboarding-api-model/pom.xml @@ -7,7 +7,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT enrollment-server-onboarding-api-model diff --git a/enrollment-server-onboarding-common/pom.xml b/enrollment-server-onboarding-common/pom.xml index 85d4054bf..09f6359c5 100644 --- a/enrollment-server-onboarding-common/pom.xml +++ b/enrollment-server-onboarding-common/pom.xml @@ -24,7 +24,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT enrollment-server-onboarding-common diff --git a/enrollment-server-onboarding-domain-model/pom.xml b/enrollment-server-onboarding-domain-model/pom.xml index a02cd9f4d..05d934ce1 100644 --- a/enrollment-server-onboarding-domain-model/pom.xml +++ b/enrollment-server-onboarding-domain-model/pom.xml @@ -30,7 +30,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index ee2ef4995..6198686a1 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -29,7 +29,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index 5cf75cc1a..ee55b0ac8 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -30,7 +30,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT diff --git a/mtoken-model/pom.xml b/mtoken-model/pom.xml index 930bfc8d1..a95dd7844 100644 --- a/mtoken-model/pom.xml +++ b/mtoken-model/pom.xml @@ -26,7 +26,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT diff --git a/pom.xml b/pom.xml index e7385f150..8b5f92b2e 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ com.wultra.security enrollment-server-parent - 1.5.0 + 1.6.0-SNAPSHOT pom From 93426c157acd52904eda55f843a8296655e2b60a Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Wed, 4 Oct 2023 08:16:22 +0200 Subject: [PATCH 03/52] Fix #880: Wrong bouncycastle version in jboss deployment --- docs/onboarding/Deploying-Wildfly.md | 2 +- .../src/main/webapp/WEB-INF/jboss-deployment-structure.xml | 2 +- .../src/main/webapp/WEB-INF/jboss-deployment-structure.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/onboarding/Deploying-Wildfly.md b/docs/onboarding/Deploying-Wildfly.md index 3e7bce397..b2653251e 100644 --- a/docs/onboarding/Deploying-Wildfly.md +++ b/docs/onboarding/Deploying-Wildfly.md @@ -15,7 +15,7 @@ Enrollment Server contains the following configuration in `jboss-deployment-stru - + diff --git a/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 2e3377e8d..e7638f97a 100644 --- a/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -8,7 +8,7 @@ - + diff --git a/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index f0b646611..86dc3331b 100644 --- a/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -8,7 +8,7 @@ - + From ba710ba486363edfeb2f4d4a40c9d17e07881507 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 5 Oct 2023 10:00:23 +0200 Subject: [PATCH 04/52] Fix #882: Filter jboss descriptor with bouncycastle version from pom --- enrollment-server-onboarding/pom.xml | 13 +++++++++++++ .../webapp/WEB-INF/jboss-deployment-structure.xml | 2 +- enrollment-server/pom.xml | 13 +++++++++++++ .../webapp/WEB-INF/jboss-deployment-structure.xml | 2 +- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index 6198686a1..cb404f237 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -244,6 +244,19 @@ + + org.apache.maven.plugins + maven-war-plugin + + + + src/main/webapp/WEB-INF + WEB-INF + true + + + + diff --git a/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index e7638f97a..ed6a86c0d 100644 --- a/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/enrollment-server-onboarding/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -8,7 +8,7 @@ - + diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index ee55b0ac8..d22135b69 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -185,6 +185,19 @@ external-service + + org.apache.maven.plugins + maven-war-plugin + + + + src/main/webapp/WEB-INF + WEB-INF + true + + + + diff --git a/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml index 86dc3331b..fe707a2a8 100644 --- a/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml +++ b/enrollment-server/src/main/webapp/WEB-INF/jboss-deployment-structure.xml @@ -8,7 +8,7 @@ - + From 2a86397de96ff9abc135fd3eb010831f1d51a4b9 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Fri, 6 Oct 2023 09:17:15 +0200 Subject: [PATCH 05/52] Fix #884: Exclude lombok from war --- enrollment-server-onboarding/pom.xml | 12 ++++++++++++ enrollment-server/pom.xml | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index cb404f237..4a32fad92 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -183,6 +183,18 @@ + + + + jakarta.servlet + jakarta.servlet-api + + + org.projectlombok + lombok + + + org.apache.maven.plugins diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index d22135b69..975117a16 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -176,6 +176,18 @@ + + + + jakarta.servlet + jakarta.servlet-api + + + org.projectlombok + lombok + + + org.apache.maven.plugins From bb28f627f59ba0bde47f07fb70100f97bb63f561 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 02:59:12 +0000 Subject: [PATCH 06/52] Bump net.javacrumbs.shedlock:shedlock-bom from 5.8.0 to 5.9.0 Bumps [net.javacrumbs.shedlock:shedlock-bom](https://github.com/lukas-krecan/ShedLock) from 5.8.0 to 5.9.0. - [Commits](https://github.com/lukas-krecan/ShedLock/compare/shedlock-parent-5.8.0...shedlock-parent-5.9.0) --- updated-dependencies: - dependency-name: net.javacrumbs.shedlock:shedlock-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7d461ce0c..1c5adcd35 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ 6.5.0 - 5.8.0 + 5.9.0 3.2.1 2.2.15 2.2.0 From fe8d58f771f6093989716cf9440e48639fa57be1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 02:35:24 +0000 Subject: [PATCH 07/52] Bump net.javacrumbs.shedlock:shedlock-bom from 5.9.0 to 5.9.1 Bumps [net.javacrumbs.shedlock:shedlock-bom](https://github.com/lukas-krecan/ShedLock) from 5.9.0 to 5.9.1. - [Commits](https://github.com/lukas-krecan/ShedLock/compare/shedlock-parent-5.9.0...shedlock-parent-5.9.1) --- updated-dependencies: - dependency-name: net.javacrumbs.shedlock:shedlock-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1c5adcd35..993735a6e 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ 6.5.0 - 5.9.0 + 5.9.1 3.2.1 2.2.15 2.2.0 From bee5c0764933059ce560acd9408bba5aeab8b830 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 02:35:42 +0000 Subject: [PATCH 08/52] Bump org.springframework.boot:spring-boot-starter-parent Bumps [org.springframework.boot:spring-boot-starter-parent](https://github.com/spring-projects/spring-boot) from 3.1.4 to 3.1.5. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.1.4...v3.1.5) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-parent dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1c5adcd35..a8251124c 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.4 + 3.1.5 From 337a84dd499b6654266d8a599b5701dde3a7c4ac Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Tue, 24 Oct 2023 10:20:33 +0200 Subject: [PATCH 09/52] Fix #891: Change defaulting of locales for template finding --- .../database/OperationTemplateRepository.java | 11 ++ .../service/OperationTemplateService.java | 5 +- .../service/OperationTemplateServiceTest.java | 102 ++++++++++++++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/OperationTemplateRepository.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/OperationTemplateRepository.java index ea0b6a013..40c605567 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/OperationTemplateRepository.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/database/OperationTemplateRepository.java @@ -41,4 +41,15 @@ public interface OperationTemplateRepository extends CrudRepository findFirstByLanguageAndPlaceholder(String language, String placeholder); + /** + * Find an operation template by the given operation type. + *

+ * Just a fallback method when no entry found by {@link #findFirstByLanguageAndPlaceholder(String, String)}. + * + * @param placeholder operation type + * @return operation template or empty + * @see #findFirstByLanguageAndPlaceholder(String, String) + */ + Optional findFirstByPlaceholder(String placeholder); + } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java index 9ff169942..0c135835e 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java @@ -53,7 +53,10 @@ public OperationTemplateService(OperationTemplateRepository operationTemplateRep public Optional findTemplate(@NotNull String operationType, @NotNull String language) { return operationTemplateRepository.findFirstByLanguageAndPlaceholder(language, operationType).or(() -> { logger.debug("Trying fallback to EN locale for operationType={}", operationType); - return operationTemplateRepository.findFirstByLanguageAndPlaceholder("en", operationType); + return operationTemplateRepository.findFirstByLanguageAndPlaceholder("en", operationType).or(() -> { + logger.debug("Trying fallback to any locale for operationType={}", operationType); + return operationTemplateRepository.findFirstByPlaceholder(operationType); + }); }); } diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java new file mode 100644 index 000000000..a4a563ee0 --- /dev/null +++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java @@ -0,0 +1,102 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.enrollmentserver.impl.service; + +import com.wultra.app.enrollmentserver.database.OperationTemplateRepository; +import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +/** + * Test for {@link OperationTemplateService}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ExtendWith(MockitoExtension.class) +class OperationTemplateServiceTest { + + @Mock + private OperationTemplateRepository dao; + + @InjectMocks + private OperationTemplateService tested; + + @Test + void testFindTemplate_givenLanguage() { + final OperationTemplateEntity entity = new OperationTemplateEntity(); + when(dao.findFirstByLanguageAndPlaceholder("cs", "myTemplate")) + .thenReturn(Optional.of(entity)); + + final Optional result = tested.findTemplate("myTemplate", "cs"); + + assertTrue(result.isPresent()); + assertEquals(entity, result.get()); + } + + @Test + void testFindTemplate_fallbackToEnglish() { + final OperationTemplateEntity entity = new OperationTemplateEntity(); + when(dao.findFirstByLanguageAndPlaceholder("cs", "myTemplate")) + .thenReturn(Optional.empty()); + when(dao.findFirstByLanguageAndPlaceholder("en", "myTemplate")) + .thenReturn(Optional.of(entity)); + + final Optional result = tested.findTemplate("myTemplate", "cs"); + + assertTrue(result.isPresent()); + assertEquals(entity, result.get()); + } + + @Test + void testFindTemplate_fallbackToAnyLanguage() { + final OperationTemplateEntity entity = new OperationTemplateEntity(); + when(dao.findFirstByLanguageAndPlaceholder("cs", "myTemplate")) + .thenReturn(Optional.empty()); + when(dao.findFirstByLanguageAndPlaceholder("en", "myTemplate")) + .thenReturn(Optional.empty()); + when(dao.findFirstByPlaceholder("myTemplate")) + .thenReturn(Optional.of(entity)); + + final Optional result = tested.findTemplate("myTemplate", "cs"); + + assertTrue(result.isPresent()); + assertEquals(entity, result.get()); + } + + @Test + void testFindTemplate_notFound() { + when(dao.findFirstByLanguageAndPlaceholder("cs", "myTemplate")) + .thenReturn(Optional.empty()); + when(dao.findFirstByLanguageAndPlaceholder("en", "myTemplate")) + .thenReturn(Optional.empty()); + when(dao.findFirstByPlaceholder("myTemplate")) + .thenReturn(Optional.empty()); + + final Optional result = tested.findTemplate("myTemplate", "cs"); + + assertFalse(result.isPresent()); + } +} From 11c3d76d2a624087f7107601671af1107735709b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Wed, 25 Oct 2023 15:08:22 +0200 Subject: [PATCH 10/52] Optimize defaulting of locales for template finding (#895) * Optimize defaulting of locales for template finding A follow-up to #891 --- .../service/OperationTemplateService.java | 32 +++++++++++++++---- .../service/OperationTemplateServiceTest.java | 18 ++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java index 0c135835e..9aafce226 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateService.java @@ -36,6 +36,8 @@ @Slf4j public class OperationTemplateService { + private static final String DEFAULT_LANGUAGE = "en"; + private final OperationTemplateRepository operationTemplateRepository; @Autowired @@ -44,20 +46,36 @@ public OperationTemplateService(OperationTemplateRepository operationTemplateRep } /** - * Find the operation template for the given type and language. Falling back to EN locale. + * Find the operation template for the given type and language. + *

+ * Falling back to EN locale and later on to any found language. * * @param operationType Operation type. * @param language Template language. * @return Found operation template or empty. */ public Optional findTemplate(@NotNull String operationType, @NotNull String language) { - return operationTemplateRepository.findFirstByLanguageAndPlaceholder(language, operationType).or(() -> { + return operationTemplateRepository.findFirstByLanguageAndPlaceholder(language, operationType).or(() -> + findTemplateFallback(operationType, language)); + } + + private Optional findTemplateFallback(final String operationType, final String language) { + if (!DEFAULT_LANGUAGE.equals(language)) { logger.debug("Trying fallback to EN locale for operationType={}", operationType); - return operationTemplateRepository.findFirstByLanguageAndPlaceholder("en", operationType).or(() -> { - logger.debug("Trying fallback to any locale for operationType={}", operationType); - return operationTemplateRepository.findFirstByPlaceholder(operationType); - }); - }); + return findDefaultTemplate(operationType); + } else { + return findAnyTemplate(operationType); + } + } + + private Optional findDefaultTemplate(final String operationType) { + return operationTemplateRepository.findFirstByLanguageAndPlaceholder(DEFAULT_LANGUAGE, operationType).or(() -> + findAnyTemplate(operationType)); + } + + private Optional findAnyTemplate(final String operationType) { + logger.debug("Trying fallback to any locale for operationType={}", operationType); + return operationTemplateRepository.findFirstByPlaceholder(operationType); } } diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java index a4a563ee0..dd6a2fc32 100644 --- a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java +++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/OperationTemplateServiceTest.java @@ -28,7 +28,7 @@ import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * Test for {@link OperationTemplateService}. @@ -86,6 +86,22 @@ void testFindTemplate_fallbackToAnyLanguage() { assertEquals(entity, result.get()); } + @Test + void testFindTemplate_fallbackToAnyLanguage_optimizationOfEnglishLocale() { + final OperationTemplateEntity entity = new OperationTemplateEntity(); + when(dao.findFirstByLanguageAndPlaceholder("en", "myTemplate")) + .thenReturn(Optional.empty()); + when(dao.findFirstByPlaceholder("myTemplate")) + .thenReturn(Optional.of(entity)); + + final Optional result = tested.findTemplate("myTemplate", "en"); + + assertTrue(result.isPresent()); + assertEquals(entity, result.get()); + + verify(dao, times(1)).findFirstByLanguageAndPlaceholder("en", "myTemplate"); + } + @Test void testFindTemplate_notFound() { when(dao.findFirstByLanguageAndPlaceholder("cs", "myTemplate")) From 16674ebd896c99b4279bd43913274a42f1fba618 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Mon, 30 Oct 2023 10:24:43 +0100 Subject: [PATCH 11/52] Fix #899: Remove implicit commons-text dependency --- enrollment-server/pom.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index 975117a16..f83dfcf4a 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -111,13 +111,6 @@ postgresql - - - org.apache.commons - commons-text - 1.10.0 - - net.logstash.logback From 499d181e9fa3c5a72d9cd06aef39ccb14b99015a Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Mon, 30 Oct 2023 14:58:26 +0100 Subject: [PATCH 12/52] Fix #902: Update Wultra dependencies to SNAPSHOT version --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 927da56bc..aaf05b1be 100644 --- a/pom.xml +++ b/pom.xml @@ -92,10 +92,10 @@ 2.2.0 1.4.2 - 1.7.0 - 1.5.1 - 1.5.0 - 1.5.0 + 1.8.0-SNAPSHOT + 1.6.0-SNAPSHOT + 1.6.0-SNAPSHOT + 1.6.0-SNAPSHOT 1.76 7.4 From 026c70f950145f01b84f578279061ee96b525264 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Tue, 31 Oct 2023 11:16:38 +0100 Subject: [PATCH 13/52] Update commons-lang3 to 3.13.0 --- pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pom.xml b/pom.xml index aaf05b1be..53bbf72aa 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,9 @@ 2.2.0 1.4.2 + + 3.13.0 + 1.8.0-SNAPSHOT 1.6.0-SNAPSHOT 1.6.0-SNAPSHOT From c734bdf3ab37a8a65471ff76ed0dfd609dc73395 Mon Sep 17 00:00:00 2001 From: Jan Dusil <134381434+jandusil@users.noreply.github.com> Date: Thu, 2 Nov 2023 00:26:04 -0600 Subject: [PATCH 14/52] Fix #889: Executable war (#890) * Fix #889: Executable war --- docs-private/Developer-How-To-Start.md | 2 -- enrollment-server-onboarding/pom.xml | 16 ---------------- enrollment-server/pom.xml | 16 ---------------- pom.xml | 15 +++++++-------- 4 files changed, 7 insertions(+), 42 deletions(-) diff --git a/docs-private/Developer-How-To-Start.md b/docs-private/Developer-How-To-Start.md index b59929d3b..3a0eaad72 100644 --- a/docs-private/Developer-How-To-Start.md +++ b/docs-private/Developer-How-To-Start.md @@ -6,7 +6,6 @@ ### Standalone Run -- Enable maven profile `standalone` - Use IntelliJ Idea run configuration at `../.run/EnrollmentServerApplication.run.xml` - Open [http://localhost:8081/enrollment-server/actuator/health](http://localhost:8081/enrollment-server/actuator/health) and you should get `{"status":"UP"}` @@ -59,7 +58,6 @@ docker run -p 80:8080 -e ENROLLMENT_SERVER_DATASOURCE_URL='jdbc:postgresql://hos ### Standalone Run -- Enable maven profile `standalone` - Use IntelliJ Idea run configuration at `../.run/EnrollmentServerOnboardingApplication.run.xml` - Open [http://localhost:8083/enrollment-server-onboarding/actuator/health](http://localhost:8083/enrollment-server-onboarding/actuator/health) and you should get `{"status":"UP"}` diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index 4a32fad92..324935d90 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -282,22 +282,6 @@ -Xdoclint:none - - standalone - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - - org.apache.tomcat.embed - tomcat-embed-el - provided - - - public-repository diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index f83dfcf4a..11ef15165 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -216,22 +216,6 @@ -Xdoclint:none - - standalone - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - - org.apache.tomcat.embed - tomcat-embed-el - provided - - - public-repository diff --git a/pom.xml b/pom.xml index 53bbf72aa..0fa5c095b 100644 --- a/pom.xml +++ b/pom.xml @@ -164,12 +164,6 @@ io.getlime.security powerauth-restful-security-spring-annotation ${powerauth-restful-integration.version} - - - org.springframework.boot - spring-boot-starter-tomcat - - @@ -187,8 +181,13 @@ org.apache.tomcat.embed tomcat-embed-el - ${tomcat.version} - test + provided + + + + org.springframework.boot + spring-boot-starter-tomcat + provided From 6e6ddc6d8d899f1e1cd012482a79b083aea9e480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Mon, 6 Nov 2023 10:27:39 +0100 Subject: [PATCH 15/52] Fix #896: Use different error when PAS operation approve fails (#901) * Fix #896: Use a different error when PAS operation approve fails --- docs/Mobile-Token-API.md | 19 ++++++----- .../DefaultExceptionHandler.java | 7 ++-- .../MobileTokenAuthException.java | 7 +++- .../impl/service/MobileTokenService.java | 33 +++++++++---------- .../mtoken/model/enumeration/ErrorCode.java | 15 +++------ 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/docs/Mobile-Token-API.md b/docs/Mobile-Token-API.md index a7ddf261e..a3eaa723c 100644 --- a/docs/Mobile-Token-API.md +++ b/docs/Mobile-Token-API.md @@ -75,15 +75,16 @@ Mobile token API provides access to operations. List of error codes in Mobile Token API: -| Code | Description | HTTP Status Code | -|---|---|---| -| `INVALID_REQUEST` | Invalid request sent - missing request object in request | 400 | -| `INVALID_ACTIVATION` | Activation is not valid (it is different from configured activation). Return this error in case the activation does not exist, or in case the activation is not allowed to perform the action (for example, user did not allow operation approvals on such device). | 400 | -| `POWERAUTH_AUTH_FAIL` | PowerAuth authentication failed | 401 | -| `OPERATION_ALREADY_FINISHED` | Operation is already finished | 400 | -| `OPERATION_ALREADY_FAILED` | Operation is already failed | 400 | -| `OPERATION_ALREADY_CANCELED` | Operation is already canceled | 400 | -| `OPERATION_EXPIRED` | Operation is expired | 400 | +| Code | Description | HTTP Status Code | +|------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------| +| `INVALID_REQUEST` | Invalid request sent - missing request object in request | 400 | +| `INVALID_ACTIVATION` | Activation is not valid (it is different from configured activation). Return this error in case the activation does not exist, or in case the activation is not allowed to perform the action (for example, user did not allow operation approvals on such device). | 400 | +| `POWERAUTH_AUTH_FAIL` | PowerAuth authentication failed | 401 | +| `OPERATION_ALREADY_FINISHED` | Operation is already finished | 400 | +| `OPERATION_ALREADY_FAILED` | Operation is already failed | 400 | +| `OPERATION_ALREADY_CANCELED` | Operation is already canceled | 400 | +| `OPERATION_EXPIRED` | Operation is expired | 400 | +| `OPERATION_FAILED` | PowerAuth server operation approval fails. | 401 | ## Localization diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/DefaultExceptionHandler.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/DefaultExceptionHandler.java index 685fd7542..80ac42a4c 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/DefaultExceptionHandler.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/DefaultExceptionHandler.java @@ -18,6 +18,7 @@ package com.wultra.app.enrollmentserver.errorhandling; +import com.wultra.security.powerauth.lib.mtoken.model.enumeration.ErrorCode; import io.getlime.core.rest.model.base.response.ErrorResponse; import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthAuthenticationException; import lombok.extern.slf4j.Slf4j; @@ -57,7 +58,7 @@ public class DefaultExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleInvalidRequestException(InvalidRequestObjectException ex) { logger.warn("Error occurred when processing request object.", ex); - return new ErrorResponse("INVALID_REQUEST", "Invalid request object."); + return new ErrorResponse(ErrorCode.INVALID_REQUEST, "Invalid request object."); } /** @@ -69,7 +70,7 @@ public class DefaultExceptionHandler { @ResponseStatus(HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handlePushRegistrationException(PushRegistrationFailedException ex) { logger.warn("Error occurred when registering to push server.", ex); - return new ErrorResponse("PUSH_REGISTRATION_FAILED", "Push registration failed in Mobile Token API component."); + return new ErrorResponse(ErrorCode.PUSH_REGISTRATION_FAILED, "Push registration failed in Mobile Token API component."); } /** @@ -81,7 +82,7 @@ public class DefaultExceptionHandler { @ResponseStatus(HttpStatus.UNAUTHORIZED) public @ResponseBody ErrorResponse handleUnauthorizedException(PowerAuthAuthenticationException ex) { logger.warn("Unable to verify device registration - authentication failed.", ex); - return new ErrorResponse("POWERAUTH_AUTH_FAIL", "Unable to verify device registration."); + return new ErrorResponse(ErrorCode.POWERAUTH_AUTH_FAIL, "Unable to verify device registration."); } /** diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/MobileTokenAuthException.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/MobileTokenAuthException.java index d2f5810a5..28fdf0ec2 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/MobileTokenAuthException.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/MobileTokenAuthException.java @@ -18,6 +18,8 @@ package com.wultra.app.enrollmentserver.errorhandling; +import com.wultra.security.powerauth.lib.mtoken.model.enumeration.ErrorCode; + import java.io.Serial; /** @@ -31,7 +33,10 @@ public class MobileTokenAuthException extends MobileTokenException { private static final long serialVersionUID = -4602362062047233809L; public MobileTokenAuthException() { - super("POWERAUTH_AUTH_FAIL", "Authentication failed"); + super(ErrorCode.POWERAUTH_AUTH_FAIL, "Authentication failed"); } + public MobileTokenAuthException(final String code, final String message) { + super(code, message); + } } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java index c5aaa297d..956032faf 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java @@ -25,7 +25,6 @@ import com.wultra.app.enrollmentserver.impl.service.converter.MobileTokenConverter; import com.wultra.core.http.common.request.RequestContext; import com.wultra.security.powerauth.client.PowerAuthClient; -import com.wultra.security.powerauth.client.model.enumeration.OperationStatus; import com.wultra.security.powerauth.client.model.enumeration.SignatureType; import com.wultra.security.powerauth.client.model.enumeration.UserActionResult; import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; @@ -35,6 +34,7 @@ import com.wultra.security.powerauth.client.model.response.OperationDetailResponse; import com.wultra.security.powerauth.client.model.response.OperationUserActionResponse; import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation; +import com.wultra.security.powerauth.lib.mtoken.model.enumeration.ErrorCode; import com.wultra.security.powerauth.lib.mtoken.model.response.OperationListResponse; import io.getlime.core.rest.model.base.response.Response; import io.getlime.security.powerauth.rest.api.spring.service.HttpCustomizationService; @@ -181,8 +181,8 @@ public Response operationApprove(@NotNull final OperationApproveParameterObject return new Response(); } else { final OperationDetailResponse operation = approveResponse.getOperation(); - handleStatus(operation.getStatus()); - throw new MobileTokenAuthException(); + handleStatus(operation); + throw new MobileTokenAuthException(ErrorCode.OPERATION_FAILED, "PowerAuth server operation approval fails"); } } @@ -208,7 +208,7 @@ public void operationFailApprove(@NotNull String operationId, @NotNull RequestCo ); final OperationDetailResponse operation = failApprovalResponse.getOperation(); - handleStatus(operation.getStatus()); + handleStatus(operation); } /** @@ -262,8 +262,8 @@ public Response operationReject( return new Response(); } else { final OperationDetailResponse operation = rejectResponse.getOperation(); - handleStatus(operation.getStatus()); - throw new MobileTokenAuthException(); + handleStatus(operation); + throw new MobileTokenAuthException(ErrorCode.OPERATION_FAILED, "PowerAuth server operation rejection fails"); } } @@ -285,7 +285,7 @@ private OperationDetailResponse getOperationDetail(String operationId) throws Po httpCustomizationService.getQueryParams(), httpCustomizationService.getHttpHeaders() ); - handleStatus(operationDetail.getStatus()); + handleStatus(operationDetail); return operationDetail; } @@ -297,22 +297,21 @@ private OperationDetailResponse getOperationDetail(String operationId) throws Po *

  • CANCELLED, APPROVED, REJECTED, or EXPIRED - throws exception with appropriate code and message.
  • * * - * @param status Operation status. + * @param operation Operation detail. * @throws MobileTokenException In case operation is in status that does not allow processing, the method throws appropriate exception. */ - private void handleStatus(OperationStatus status) throws MobileTokenException { - switch (status) { - case PENDING -> { - // OK, this operation is still pending - } + private static void handleStatus(final OperationDetailResponse operation) throws MobileTokenException { + switch (operation.getStatus()) { + case PENDING -> + logger.debug("OK, operation ID: {} is still pending", operation.getId()); case CANCELED -> - throw new MobileTokenException("OPERATION_ALREADY_CANCELED", "Operation was already canceled"); + throw new MobileTokenException(ErrorCode.OPERATION_ALREADY_CANCELED, "Operation was already canceled"); case APPROVED, REJECTED -> - throw new MobileTokenException("OPERATION_ALREADY_FINISHED", "Operation was already completed"); + throw new MobileTokenException(ErrorCode.OPERATION_ALREADY_FINISHED, "Operation was already completed"); case FAILED -> - throw new MobileTokenException("OPERATION_ALREADY_FAILED", "Operation already failed"); + throw new MobileTokenException(ErrorCode.OPERATION_ALREADY_FAILED, "Operation already failed"); default -> - throw new MobileTokenException("OPERATION_EXPIRED", "Operation already expired"); + throw new MobileTokenException(ErrorCode.OPERATION_EXPIRED, "Operation already expired"); } } diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/enumeration/ErrorCode.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/enumeration/ErrorCode.java index e14e922af..a6ef3225a 100644 --- a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/enumeration/ErrorCode.java +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/enumeration/ErrorCode.java @@ -36,16 +36,6 @@ public class ErrorCode { */ public static final String INVALID_REQUEST = "INVALID_REQUEST"; - /** - * Error code for situation when an activation is not active. - */ - public static final String ACTIVATION_NOT_ACTIVE = "ACTIVATION_NOT_ACTIVE"; - - /** - * Error code for situation when an activation is not configured. - */ - public static final String ACTIVATION_NOT_CONFIGURED = "ACTIVATION_NOT_CONFIGURED"; - /** * Error code for situation when an invalid activation / device is * attempted for operation manipulation. @@ -75,6 +65,11 @@ public class ErrorCode { */ public static final String OPERATION_ALREADY_CANCELED = "OPERATION_ALREADY_CANCELED"; + /** + * Error code for situation when PowerAuth server operation approval fails. + */ + public static final String OPERATION_FAILED = "OPERATION_FAILED"; + /** * Error code for situation when an operation expired and yet, some further * action was requested with that operation. From 67f89a56b2ff07fccec69cd659fb19bf19411a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pe=C5=A1ek?= Date: Wed, 8 Nov 2023 10:15:55 +0100 Subject: [PATCH 16/52] Fix #905: HHH90000025: PostgreSQLDialect does not need to be specified explicitly (#906) --- docs/Configuration-Properties.md | 1 - docs/onboarding/Configuration-Properties.md | 1 - .../src/main/resources/application.properties | 2 -- .../src/test/resources/application-test.properties | 1 - enrollment-server/src/main/resources/application.properties | 2 -- 5 files changed, 7 deletions(-) diff --git a/docs/Configuration-Properties.md b/docs/Configuration-Properties.md index c0d5a0187..9a0295a8d 100644 --- a/docs/Configuration-Properties.md +++ b/docs/Configuration-Properties.md @@ -10,7 +10,6 @@ The Enrollment Server uses the following public configuration properties: | `spring.datasource.username` | `_empty_` | Database JDBC username | | `spring.datasource.password` | `_empty_` | Database JDBC password | | `spring.datasource.driver-class-name` | `_empty_` | Datasource JDBC class name | -| `spring.jpa.database-platform` | `_empty_` | Database dialect | | `spring.jpa.hibernate.ddl-auto` | `none` | Configuration of automatic database schema creation | | `spring.jpa.properties.hibernate.connection.characterEncoding` | `_empty_` | Character encoding | | `spring.jpa.properties.hibernate.connection.useUnicode` | `_empty_` | Character encoding - Unicode support | diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index a9c3e1dd5..214c090b6 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -10,7 +10,6 @@ The Onboarding Server uses the following public configuration properties: | `spring.datasource.username` | `powerauth` | Database JDBC username | | `spring.datasource.password` | `_empty_` | Database JDBC password | | `spring.datasource.driver-class-name` | `org.postgresql.Driver` | Datasource JDBC class name | -| `spring.jpa.database-platform` | `org.hibernate.dialect.PostgreSQLDialect` | Database dialect | | `spring.jpa.hibernate.ddl-auto` | `none` | Configuration of automatic database schema creation | | `spring.jpa.properties.hibernate.connection.characterEncoding` | `utf8` | Character encoding | | `spring.jpa.properties.hibernate.connection.useUnicode` | `true` | Character encoding - Unicode support | diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index e24027d54..ebb5fd26d 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -27,7 +27,6 @@ spring.datasource.username=powerauth spring.datasource.password= spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.hikari.auto-commit=false -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.connection.characterEncoding=utf8 spring.jpa.properties.hibernate.connection.useUnicode=true @@ -36,7 +35,6 @@ spring.jpa.properties.hibernate.connection.useUnicode=true #spring.datasource.username=powerauth #spring.datasource.password= #spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver -#spring.jpa.database-platform=org.hibernate.dialect.OracleDialect # Hibernate Configuration spring.jpa.hibernate.ddl-auto=none diff --git a/enrollment-server-onboarding/src/test/resources/application-test.properties b/enrollment-server-onboarding/src/test/resources/application-test.properties index dce7de403..50a2b529f 100644 --- a/enrollment-server-onboarding/src/test/resources/application-test.properties +++ b/enrollment-server-onboarding/src/test/resources/application-test.properties @@ -19,7 +19,6 @@ spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE spring.datasource.username=sa spring.datasource.password=password spring.datasource.driver-class-name=org.h2.Driver -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect spring.jpa.hibernate.ddl-auto=create spring.liquibase.enabled=false diff --git a/enrollment-server/src/main/resources/application.properties b/enrollment-server/src/main/resources/application.properties index 4ba2ee175..76c0634e0 100644 --- a/enrollment-server/src/main/resources/application.properties +++ b/enrollment-server/src/main/resources/application.properties @@ -27,7 +27,6 @@ spring.datasource.username=powerauth spring.datasource.password= spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.hikari.auto-commit=false -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.connection.characterEncoding=utf8 spring.jpa.properties.hibernate.connection.useUnicode=true @@ -36,7 +35,6 @@ spring.jpa.properties.hibernate.connection.useUnicode=true #spring.datasource.username=powerauth #spring.datasource.password= #spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver -#spring.jpa.database-platform=org.hibernate.dialect.OracleDialect # Hibernate Configuration spring.jpa.hibernate.ddl-auto=none From 72e68fd528e515bdc698ffa0aea99fbc5d3bf0a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 02:15:49 +0000 Subject: [PATCH 17/52] Bump net.javacrumbs.shedlock:shedlock-bom from 5.9.1 to 5.10.0 Bumps [net.javacrumbs.shedlock:shedlock-bom](https://github.com/lukas-krecan/ShedLock) from 5.9.1 to 5.10.0. - [Commits](https://github.com/lukas-krecan/ShedLock/compare/shedlock-parent-5.9.1...shedlock-parent-5.10.0) --- updated-dependencies: - dependency-name: net.javacrumbs.shedlock:shedlock-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0fa5c095b..dcb4dee3b 100644 --- a/pom.xml +++ b/pom.xml @@ -86,7 +86,7 @@ 6.5.0 - 5.9.1 + 5.10.0 3.2.1 2.2.15 2.2.0 From 6b46e8a52ee0ac5bb4c604abdc49355b64022a8b Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 16 Nov 2023 11:34:53 +0100 Subject: [PATCH 18/52] Fix #914: Operation history fails for big response --- .../impl/service/MobileTokenService.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java index 956032faf..8d70fac99 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java @@ -109,15 +109,17 @@ public OperationListResponse operationListForUser( final OperationListForUserRequest request = new OperationListForUserRequest(); request.setUserId(userId); request.setApplications(List.of(applicationId)); + request.setPageNumber(0); + request.setPageSize(OPERATION_LIST_LIMIT); final MultiValueMap queryParams = httpCustomizationService.getQueryParams(); final MultiValueMap httpHeaders = httpCustomizationService.getHttpHeaders(); - final com.wultra.security.powerauth.client.model.response.OperationListResponse pendingList = + final com.wultra.security.powerauth.client.model.response.OperationListResponse operations = pendingOnly ? powerAuthClient.operationPendingList(request, queryParams, httpHeaders) : powerAuthClient.operationList(request, queryParams, httpHeaders); final OperationListResponse responseObject = new OperationListResponse(); - for (OperationDetailResponse operationDetail: pendingList) { + for (OperationDetailResponse operationDetail: operations) { final String activationFlag = operationDetail.getActivationFlag(); if (activationFlag == null || activationFlags.contains(activationFlag)) { // only return data if there is no flag, or if flag matches flags of activation final Optional operationTemplate = operationTemplateService.findTemplate(operationDetail.getOperationType(), language); @@ -127,10 +129,6 @@ public OperationListResponse operationListForUser( } final Operation operation = mobileTokenConverter.convert(operationDetail, operationTemplate.get()); responseObject.add(operation); - if (responseObject.size() >= OPERATION_LIST_LIMIT) { - logger.info("Reached the limit of operation list ({}) for user ID: {}", OPERATION_LIST_LIMIT, userId); - break; - } } } return responseObject; From 890741264b5d846beedc9c8f9b9918de96b3d7a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 02:31:24 +0000 Subject: [PATCH 19/52] Bump org.bouncycastle:bcprov-jdk18on from 1.76 to 1.77 Bumps [org.bouncycastle:bcprov-jdk18on](https://github.com/bcgit/bc-java) from 1.76 to 1.77. - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) --- updated-dependencies: - dependency-name: org.bouncycastle:bcprov-jdk18on dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index dcb4dee3b..24ca98b4b 100644 --- a/pom.xml +++ b/pom.xml @@ -100,7 +100,7 @@ 1.6.0-SNAPSHOT 1.6.0-SNAPSHOT - 1.76 + 1.77 7.4 From d44bacc94f24aaecbb3b7d674c32eef555244e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Wed, 22 Nov 2023 13:11:46 +0100 Subject: [PATCH 20/52] Fix #923: Introduce maven modules enrollment-onboarding-api, -zenid, and -iproov (#924) * Fix #923: Introduce maven modules enrollment-onboarding-api, -zenid, and -iproov --- enrollment-server-onboarding-api/pom.xml | 40 + .../DocumentVerificationException.java | 2 +- .../errorhandling/PresenceCheckException.java | 2 +- .../DocumentVerificationProvider.java | 6 +- .../api}/provider/PresenceCheckProvider.java | 6 +- .../pom.xml | 92 + .../provider/iproov}/IProovConfig.java | 8 +- .../provider/iproov}/IProovConfigProps.java | 6 +- .../iproov}/IProovPresenceCheckProvider.java | 17 +- .../iproov}/IProovRestApiService.java | 11 +- .../src/main/resources/api/api-iproov.json | 0 .../EnrollmentServerTestApplication.java | 21 +- .../IProovPresenceCheckProviderTest.java | 27 +- .../iproov}/IProovRestApiServiceTest.java | 7 +- .../application-external-service.properties | 3 + .../pom.xml | 93 + .../CustomOffsetDateTimeDeserializer.java | 4 +- .../provider/zenid}/ZenidConfig.java | 7 +- .../provider/zenid}/ZenidConfigProps.java | 6 +- .../ZenidDocumentVerificationProvider.java | 20 +- .../provider/zenid}/ZenidRestApiService.java | 7 +- .../src/main/resources/api/api-zenid.yaml | 0 .../EnrollmentServerTestApplication.java | 28 + ...ZenidDocumentVerificationProviderTest.java | 61 +- .../zenid}/ZenidRestApiServiceTest.java | 8 +- .../application-external-service.properties | 7 + .../resources/application-test.properties | 5 + enrollment-server-onboarding/pom.xml | 78 +- .../api/IdentityVerificationController.java | 4 +- ...ultraMockDocumentVerificationProvider.java | 4 +- .../DefaultExceptionHandler.java | 2 + .../impl/service/DataExtractionService.java | 2 +- .../IdentityVerificationRestService.java | 4 +- .../service/IdentityVerificationService.java | 4 +- .../impl/service/ImageProcessor.java | 2 +- .../impl/service/PresenceCheckService.java | 6 +- .../document/DocumentProcessingService.java | 4 +- .../document/DocumentVerificationService.java | 4 +- .../VerificationProcessingBatchService.java | 4 +- .../VerificationProcessingService.java | 2 +- .../WultraMockPresenceCheckProvider.java | 2 +- .../PresenceCheckInitAction.java | 2 + .../PresenceCheckVerificationAction.java | 2 +- .../DocumentVerificationFinalAction.java | 2 +- .../VerificationDocumentStartAction.java | 2 +- .../CustomStateMachineInterceptor.java | 2 + .../src/main/resources/api/api-zenid.json | 2961 ----------------- ...tractDocumentVerificationProviderTest.java | 53 - ...aMockDocumentVerificationProviderTest.java | 22 +- .../application-external-service.properties | 9 - enrollment-server/pom.xml | 8 - pom.xml | 35 +- 52 files changed, 496 insertions(+), 3218 deletions(-) create mode 100644 enrollment-server-onboarding-api/pom.xml rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver => enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api}/errorhandling/DocumentVerificationException.java (95%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver => enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api}/errorhandling/PresenceCheckException.java (95%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver => enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api}/provider/DocumentVerificationProvider.java (96%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver => enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api}/provider/PresenceCheckProvider.java (94%) create mode 100644 enrollment-server-onboarding-provider-iproov/pom.xml rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config => enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov}/IProovConfig.java (98%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config => enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov}/IProovConfigProps.java (93%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider => enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov}/IProovPresenceCheckProvider.java (93%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/service => enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov}/IProovRestApiService.java (96%) rename {enrollment-server-onboarding => enrollment-server-onboarding-provider-iproov}/src/main/resources/api/api-iproov.json (100%) rename enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/ZenidConst.java => enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/EnrollmentServerTestApplication.java (64%) rename {enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider => enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov}/IProovPresenceCheckProviderTest.java (84%) rename {enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/service => enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov}/IProovRestApiServiceTest.java (81%) create mode 100644 enrollment-server-onboarding-provider-iproov/src/test/resources/application-external-service.properties create mode 100644 enrollment-server-onboarding-provider-zenid/pom.xml rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/model/deserializer => enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid}/CustomOffsetDateTimeDeserializer.java (91%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config => enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid}/ZenidConfig.java (94%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config => enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid}/ZenidConfigProps.java (93%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/provider => enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid}/ZenidDocumentVerificationProvider.java (97%) rename {enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/service => enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid}/ZenidRestApiService.java (98%) rename {enrollment-server-onboarding => enrollment-server-onboarding-provider-zenid}/src/main/resources/api/api-zenid.yaml (100%) create mode 100644 enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/EnrollmentServerTestApplication.java rename {enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/provider => enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid}/ZenidDocumentVerificationProviderTest.java (79%) rename {enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/service => enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid}/ZenidRestApiServiceTest.java (98%) create mode 100644 enrollment-server-onboarding-provider-zenid/src/test/resources/application-external-service.properties create mode 100644 enrollment-server-onboarding-provider-zenid/src/test/resources/application-test.properties delete mode 100644 enrollment-server-onboarding/src/main/resources/api/api-zenid.json delete mode 100644 enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/AbstractDocumentVerificationProviderTest.java delete mode 100644 enrollment-server-onboarding/src/test/resources/application-external-service.properties diff --git a/enrollment-server-onboarding-api/pom.xml b/enrollment-server-onboarding-api/pom.xml new file mode 100644 index 000000000..48cb44c2e --- /dev/null +++ b/enrollment-server-onboarding-api/pom.xml @@ -0,0 +1,40 @@ + + + + + 4.0.0 + + + com.wultra.security + enrollment-server-parent + 1.6.0-SNAPSHOT + + + com.wultra.security + enrollment-server-onboarding-api + + + + com.wultra.security + enrollment-server-onboarding-common + + + \ No newline at end of file diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/DocumentVerificationException.java b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/errorhandling/DocumentVerificationException.java similarity index 95% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/DocumentVerificationException.java rename to enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/errorhandling/DocumentVerificationException.java index 8d8043e51..b24d8825d 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/DocumentVerificationException.java +++ b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/errorhandling/DocumentVerificationException.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.errorhandling; +package com.wultra.app.onboardingserver.api.errorhandling; import java.io.Serial; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/PresenceCheckException.java b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/errorhandling/PresenceCheckException.java similarity index 95% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/PresenceCheckException.java rename to enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/errorhandling/PresenceCheckException.java index 9837c8bc0..90c319ecc 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/PresenceCheckException.java +++ b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/errorhandling/PresenceCheckException.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.errorhandling; +package com.wultra.app.onboardingserver.api.errorhandling; import java.io.Serial; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/DocumentVerificationProvider.java b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/DocumentVerificationProvider.java similarity index 96% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/DocumentVerificationProvider.java rename to enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/DocumentVerificationProvider.java index 0c2e88558..60ceb556f 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/DocumentVerificationProvider.java +++ b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/DocumentVerificationProvider.java @@ -15,13 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.provider; +package com.wultra.app.onboardingserver.api.provider; import com.wultra.app.enrollmentserver.model.integration.*; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.core.annotations.PublicSpi; import java.util.List; import java.util.Map; @@ -31,6 +32,7 @@ * * @author Roman Strobl, roman.strobl@wultra.com */ +@PublicSpi public interface DocumentVerificationProvider { /** diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/PresenceCheckProvider.java b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java similarity index 94% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/PresenceCheckProvider.java rename to enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java index 252e8cad6..f8d2af938 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/PresenceCheckProvider.java +++ b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java @@ -15,20 +15,22 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.provider; +package com.wultra.app.onboardingserver.api.provider; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult; import com.wultra.app.enrollmentserver.model.integration.SessionInfo; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException; +import com.wultra.core.annotations.PublicSpi; /** * Provider which allows customization of the presence check. * * @author Roman Strobl, roman.strobl@wultra.com */ +@PublicSpi public interface PresenceCheckProvider { /** diff --git a/enrollment-server-onboarding-provider-iproov/pom.xml b/enrollment-server-onboarding-provider-iproov/pom.xml new file mode 100644 index 000000000..3822d8b36 --- /dev/null +++ b/enrollment-server-onboarding-provider-iproov/pom.xml @@ -0,0 +1,92 @@ + + + + + 4.0.0 + + + com.wultra.security + enrollment-server-parent + 1.6.0-SNAPSHOT + + + com.wultra.security + enrollment-server-onboarding-provider-iproov + + + + com.wultra.security + enrollment-server-onboarding-api + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + test + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator-maven-plugin.version} + + + swagger-definitions-iproov + + generate + + + false + ${basedir}/src/main/resources/api/api-iproov.json + java + false + false + true + false + false + true + + native + true + com.wultra.app.onboardingserver.provider.iproov.model.api + true + + + + + + + + \ No newline at end of file diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfig.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfig.java similarity index 98% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfig.java rename to enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfig.java index 7f5655563..b1fd137cb 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfig.java +++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfig.java @@ -15,12 +15,12 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.presencecheck.iproov.config; +package com.wultra.app.onboardingserver.provider.iproov; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.AuthTokenResponse; +import com.wultra.app.onboardingserver.provider.iproov.model.api.AuthTokenResponse; import com.wultra.core.rest.client.base.DefaultRestClient; import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientConfiguration; @@ -68,10 +68,10 @@ * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ @ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "iproov") -@ComponentScan(basePackages = {"com.wultra.app.onboardingserver.presencecheck"}) +@ComponentScan(basePackages = {"com.wultra.app.onboardingserver.provider.iproov"}) @Configuration @Slf4j -public class IProovConfig { +class IProovConfig { private static final String OAUTH_REGISTRATION_ID = "iproov"; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfigProps.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfigProps.java similarity index 93% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfigProps.java rename to enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfigProps.java index 56650f489..beff6b6f8 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/config/IProovConfigProps.java +++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfigProps.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.presencecheck.iproov.config; +package com.wultra.app.onboardingserver.provider.iproov; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.ServerClaimRequest; +import com.wultra.app.onboardingserver.provider.iproov.model.api.ServerClaimRequest; import com.wultra.core.rest.client.base.RestClientConfiguration; import lombok.Getter; import lombok.Setter; @@ -35,7 +35,7 @@ @Configuration @ConfigurationProperties(prefix = "enrollment-server-onboarding.presence-check.iproov") @Getter @Setter -public class IProovConfigProps { +class IProovConfigProps { /** * API key diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider/IProovPresenceCheckProvider.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java similarity index 93% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider/IProovPresenceCheckProvider.java rename to enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java index 97061280e..aa15ce1b4 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider/IProovPresenceCheckProvider.java +++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.presencecheck.iproov.provider; +package com.wultra.app.onboardingserver.provider.iproov; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,14 +25,13 @@ import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult; import com.wultra.app.enrollmentserver.model.integration.SessionInfo; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.ClaimResponse; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.ClaimValidateResponse; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.ClientErrorResponse; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.EnrolResponse; -import com.wultra.app.onboardingserver.presencecheck.iproov.service.IProovRestApiService; -import com.wultra.app.onboardingserver.provider.PresenceCheckProvider; +import com.wultra.app.onboardingserver.provider.iproov.model.api.ClaimResponse; +import com.wultra.app.onboardingserver.provider.iproov.model.api.ClaimValidateResponse; +import com.wultra.app.onboardingserver.provider.iproov.model.api.ClientErrorResponse; +import com.wultra.app.onboardingserver.provider.iproov.model.api.EnrolResponse; import com.wultra.core.rest.client.base.RestClientException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -52,7 +51,7 @@ @ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "iproov") @Component @Slf4j -public class IProovPresenceCheckProvider implements PresenceCheckProvider { +class IProovPresenceCheckProvider implements PresenceCheckProvider { /** * Session parameter name of the verification token diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/service/IProovRestApiService.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovRestApiService.java similarity index 96% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/service/IProovRestApiService.java rename to enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovRestApiService.java index fabc95b19..22b06c1c9 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/iproov/service/IProovRestApiService.java +++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovRestApiService.java @@ -15,15 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.presencecheck.iproov.service; +package com.wultra.app.onboardingserver.provider.iproov; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.presencecheck.iproov.config.IProovConfigProps; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.ClaimValidateRequest; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.ClientErrorResponse; -import com.wultra.app.onboardingserver.presencecheck.iproov.model.api.ServerClaimRequest; +import com.wultra.app.onboardingserver.provider.iproov.model.api.ClaimValidateRequest; +import com.wultra.app.onboardingserver.provider.iproov.model.api.ClientErrorResponse; +import com.wultra.app.onboardingserver.provider.iproov.model.api.ServerClaimRequest; import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientException; import lombok.extern.slf4j.Slf4j; @@ -61,7 +60,7 @@ @ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "iproov") @Service @Slf4j -public class IProovRestApiService { +class IProovRestApiService { private static final MultiValueMap EMPTY_QUERY_PARAMS = new LinkedMultiValueMap<>(); diff --git a/enrollment-server-onboarding/src/main/resources/api/api-iproov.json b/enrollment-server-onboarding-provider-iproov/src/main/resources/api/api-iproov.json similarity index 100% rename from enrollment-server-onboarding/src/main/resources/api/api-iproov.json rename to enrollment-server-onboarding-provider-iproov/src/main/resources/api/api-iproov.json diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/ZenidConst.java b/enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/EnrollmentServerTestApplication.java similarity index 64% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/ZenidConst.java rename to enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/EnrollmentServerTestApplication.java index 7f850d876..beb4c684c 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/ZenidConst.java +++ b/enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/EnrollmentServerTestApplication.java @@ -1,6 +1,6 @@ /* * PowerAuth Enrollment Server - * Copyright (C) 2022 Wultra s.r.o. + * Copyright (C) 2021 Wultra s.r.o. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published @@ -15,23 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid; +package com.wultra.app.onboardingserver.provider.iproov; + +import org.springframework.boot.autoconfigure.SpringBootApplication; /** - * Constants for ZenID purposes - * * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ -public class ZenidConst { - - /** - * Verification SDK initialization response - */ - public static final String SDK_INIT_RESPONSE = "zenid-sdk-init-response"; - - /** - * Verification SDK initialization token - */ - public static final String SDK_INIT_TOKEN = "sdk-init-token"; +@SpringBootApplication +class EnrollmentServerTestApplication { } diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider/IProovPresenceCheckProviderTest.java b/enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProviderTest.java similarity index 84% rename from enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider/IProovPresenceCheckProviderTest.java rename to enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProviderTest.java index bf2714586..24c102d57 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/provider/IProovPresenceCheckProviderTest.java +++ b/enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProviderTest.java @@ -15,15 +15,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.presencecheck.iproov.provider; +package com.wultra.app.onboardingserver.provider.iproov; import com.wultra.app.enrollmentserver.model.enumeration.PresenceCheckStatus; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult; import com.wultra.app.enrollmentserver.model.integration.SessionInfo; -import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; -import com.wultra.app.test.TestUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -33,6 +31,9 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.test.context.ActiveProfiles; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; @@ -128,8 +129,26 @@ private OwnerId createOwnerId() { } private void initPresenceCheck(OwnerId ownerId) throws Exception { - Image photo = TestUtil.loadPhoto("/images/specimen_photo.jpg"); + final Image photo = loadPhoto("/images/specimen_photo.jpg"); provider.initPresenceCheck(ownerId, photo); } + private static Image loadPhoto(final String path) throws IOException { + final File file = new File(path); + + return Image.builder() + .data(readImageData(path)) + .filename(file.getName()) + .build(); + } + + private static byte[] readImageData(final String path) throws IOException { + try (InputStream stream = IProovPresenceCheckProviderTest.class.getResourceAsStream(path)) { + if (stream == null) { + throw new IllegalStateException("Unable to get a stream for: " + path); + } + return stream.readAllBytes(); + } + } + } diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/service/IProovRestApiServiceTest.java b/enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/IProovRestApiServiceTest.java similarity index 81% rename from enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/service/IProovRestApiServiceTest.java rename to enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/IProovRestApiServiceTest.java index b19579636..f5eecaffa 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/iproov/service/IProovRestApiServiceTest.java +++ b/enrollment-server-onboarding-provider-iproov/src/test/java/com/wultra/app/onboardingserver/provider/iproov/IProovRestApiServiceTest.java @@ -15,13 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.presencecheck.iproov.service; +package com.wultra.app.onboardingserver.provider.iproov; -import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -29,8 +26,6 @@ /** * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ -@SpringBootTest(classes = EnrollmentServerTestApplication.class) -@ActiveProfiles("test") class IProovRestApiServiceTest { @Test diff --git a/enrollment-server-onboarding-provider-iproov/src/test/resources/application-external-service.properties b/enrollment-server-onboarding-provider-iproov/src/test/resources/application-external-service.properties new file mode 100644 index 000000000..e71d0317c --- /dev/null +++ b/enrollment-server-onboarding-provider-iproov/src/test/resources/application-external-service.properties @@ -0,0 +1,3 @@ +enrollment-server-onboarding.presence-check.provider=iproov + +logging.level.root=INFO diff --git a/enrollment-server-onboarding-provider-zenid/pom.xml b/enrollment-server-onboarding-provider-zenid/pom.xml new file mode 100644 index 000000000..00c1a27ca --- /dev/null +++ b/enrollment-server-onboarding-provider-zenid/pom.xml @@ -0,0 +1,93 @@ + + + + + 4.0.0 + + + com.wultra.security + enrollment-server-parent + 1.6.0-SNAPSHOT + + + com.wultra.security + enrollment-server-onboarding-provider-zenid + + + + com.wultra.security + enrollment-server-onboarding-api + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.squareup.okhttp3 + mockwebserver + test + + + + com.h2database + h2 + test + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator-maven-plugin.version} + + + swagger-definitions-zenid + + generate + + + false + ${basedir}/src/main/resources/api/api-zenid.yaml + java + false + false + true + false + false + + native + true + com.wultra.app.onboardingserver.provider.zenid.model.api + true + + + + + + + + + \ No newline at end of file diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/model/deserializer/CustomOffsetDateTimeDeserializer.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/CustomOffsetDateTimeDeserializer.java similarity index 91% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/model/deserializer/CustomOffsetDateTimeDeserializer.java rename to enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/CustomOffsetDateTimeDeserializer.java index fd9606064..ec50836fd 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/model/deserializer/CustomOffsetDateTimeDeserializer.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/CustomOffsetDateTimeDeserializer.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid.model.deserializer; +package com.wultra.app.onboardingserver.provider.zenid; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; @@ -35,7 +35,7 @@ * The ZenID returns simple ISO data on some date elements which are expected to be date-time (e.g. BirthDate) *

    */ -public class CustomOffsetDateTimeDeserializer extends JsonDeserializer { +class CustomOffsetDateTimeDeserializer extends JsonDeserializer { @Override public OffsetDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException { diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config/ZenidConfig.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfig.java similarity index 94% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config/ZenidConfig.java rename to enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfig.java index efce6f861..a8a667614 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config/ZenidConfig.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfig.java @@ -15,11 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid.config; +package com.wultra.app.onboardingserver.provider.zenid; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import com.wultra.app.onboardingserver.docverify.zenid.model.deserializer.CustomOffsetDateTimeDeserializer; import com.wultra.core.rest.client.base.DefaultRestClient; import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientConfiguration; @@ -40,9 +39,9 @@ * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ @ConditionalOnProperty(value = "enrollment-server-onboarding.document-verification.provider", havingValue = "zenid") -@ComponentScan(basePackages = {"com.wultra.app.onboardingserver.docverify"}) +@ComponentScan(basePackages = {"com.wultra.app.onboardingserver.provider.zenid"}) @Configuration -public class ZenidConfig { +class ZenidConfig { /** * @param configProps Configuration properties diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config/ZenidConfigProps.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfigProps.java similarity index 93% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config/ZenidConfigProps.java rename to enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfigProps.java index c607baf1c..ce479eb01 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/config/ZenidConfigProps.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfigProps.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid.config; +package com.wultra.app.onboardingserver.provider.zenid; -import com.wultra.app.onboardingserver.docverify.zenid.model.api.ZenidSharedMineAllResult; +import com.wultra.app.onboardingserver.provider.zenid.model.api.ZenidSharedMineAllResult; import com.wultra.core.rest.client.base.RestClientConfiguration; import lombok.Getter; import lombok.Setter; @@ -38,7 +38,7 @@ @Configuration @ConfigurationProperties(prefix = "enrollment-server-onboarding.document-verification.zenid") @Getter @Setter -public class ZenidConfigProps { +class ZenidConfigProps { /** * // TODO consider removing this config option diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/provider/ZenidDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProvider.java similarity index 97% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/provider/ZenidDocumentVerificationProvider.java rename to enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProvider.java index e1aa5d3e0..6f766b971 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/provider/ZenidDocumentVerificationProvider.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProvider.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid.provider; +package com.wultra.app.onboardingserver.provider.zenid; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -29,12 +29,9 @@ import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.docverify.zenid.ZenidConst; -import com.wultra.app.onboardingserver.docverify.zenid.config.ZenidConfigProps; -import com.wultra.app.onboardingserver.docverify.zenid.model.api.*; -import com.wultra.app.onboardingserver.docverify.zenid.service.ZenidRestApiService; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.provider.DocumentVerificationProvider; +import com.wultra.app.onboardingserver.provider.zenid.model.api.*; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import com.wultra.core.rest.client.base.RestClientException; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; @@ -59,6 +56,9 @@ @Slf4j public class ZenidDocumentVerificationProvider implements DocumentVerificationProvider { + private static final String SDK_INIT_RESPONSE = "zenid-sdk-init-response"; + private static final String SDK_INIT_TOKEN = "sdk-init-token"; + private static final String INTERNAL_SERVER_ERROR = "InternalServerError"; private static final String LICENSE_INVALID = "License invalid"; @@ -331,8 +331,8 @@ public List parseRejectionReasons(DocumentResultEntity docResult) throws @Override public VerificationSdkInfo initVerificationSdk(OwnerId id, Map initAttributes) throws RemoteCommunicationException, DocumentVerificationException { - Preconditions.checkArgument(initAttributes.containsKey(ZenidConst.SDK_INIT_TOKEN), "Missing initialization token for ZenID SDK"); - String token = initAttributes.get(ZenidConst.SDK_INIT_TOKEN); + Preconditions.checkArgument(initAttributes.containsKey(SDK_INIT_TOKEN), "Missing initialization token for ZenID SDK"); + String token = initAttributes.get(SDK_INIT_TOKEN); ResponseEntity responseEntity; try { @@ -357,7 +357,7 @@ public VerificationSdkInfo initVerificationSdk(OwnerId id, Map i } VerificationSdkInfo verificationSdkInfo = new VerificationSdkInfo(); - verificationSdkInfo.getAttributes().put(ZenidConst.SDK_INIT_RESPONSE, response.getResponse()); + verificationSdkInfo.getAttributes().put(SDK_INIT_RESPONSE, response.getResponse()); return verificationSdkInfo; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/service/ZenidRestApiService.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiService.java similarity index 98% rename from enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/service/ZenidRestApiService.java rename to enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiService.java index be1810956..c98964a2e 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/zenid/service/ZenidRestApiService.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiService.java @@ -15,15 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid.service; +package com.wultra.app.onboardingserver.provider.zenid; import com.google.common.base.Preconditions; import com.wultra.app.enrollmentserver.model.enumeration.CardSide; import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.SubmittedDocument; -import com.wultra.app.onboardingserver.docverify.zenid.config.ZenidConfigProps; -import com.wultra.app.onboardingserver.docverify.zenid.model.api.*; +import com.wultra.app.onboardingserver.provider.zenid.model.api.*; import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientException; import jakarta.annotation.Nullable; @@ -51,7 +50,7 @@ @ConditionalOnProperty(value = "enrollment-server-onboarding.document-verification.provider", havingValue = "zenid") @Service @Slf4j -public class ZenidRestApiService { +class ZenidRestApiService { private static final MultiValueMap EMPTY_ADDITIONAL_HEADERS = new LinkedMultiValueMap<>(); diff --git a/enrollment-server-onboarding/src/main/resources/api/api-zenid.yaml b/enrollment-server-onboarding-provider-zenid/src/main/resources/api/api-zenid.yaml similarity index 100% rename from enrollment-server-onboarding/src/main/resources/api/api-zenid.yaml rename to enrollment-server-onboarding-provider-zenid/src/main/resources/api/api-zenid.yaml diff --git a/enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/EnrollmentServerTestApplication.java b/enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/EnrollmentServerTestApplication.java new file mode 100644 index 000000000..df3c048ad --- /dev/null +++ b/enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/EnrollmentServerTestApplication.java @@ -0,0 +1,28 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2021 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.zenid; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Lukas Lukovsky, lukas.lukovsky@wultra.com + */ +@SpringBootApplication +class EnrollmentServerTestApplication { + +} diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/provider/ZenidDocumentVerificationProviderTest.java b/enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProviderTest.java similarity index 79% rename from enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/provider/ZenidDocumentVerificationProviderTest.java rename to enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProviderTest.java index 23acc729d..937b9d1fa 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/provider/ZenidDocumentVerificationProviderTest.java +++ b/enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProviderTest.java @@ -15,18 +15,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid.provider; +package com.wultra.app.onboardingserver.provider.zenid; import com.wultra.app.enrollmentserver.model.enumeration.CardSide; import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; import com.wultra.app.enrollmentserver.model.integration.*; -import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; -import com.wultra.app.onboardingserver.docverify.AbstractDocumentVerificationProviderTest; -import com.wultra.app.onboardingserver.docverify.zenid.ZenidConst; -import com.wultra.app.test.TestUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; @@ -41,14 +37,16 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.test.context.ActiveProfiles; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNull; /** * @author Lukas Lukovsky, lukas.lukovsky@wultra.com @@ -58,7 +56,7 @@ @ComponentScan(basePackages = {"com.wultra.app.onboardingserver.docverify.zenid"}) @EnableConfigurationProperties @Tag("external-service") -class ZenidDocumentVerificationProviderTest extends AbstractDocumentVerificationProviderTest { +class ZenidDocumentVerificationProviderTest { private static final Logger logger = LoggerFactory.getLogger(ZenidDocumentVerificationProviderTest.class); @@ -183,13 +181,13 @@ void parseRejectionReasonsTest() throws Exception { @Test void initVerificationSdkTest() throws Exception { - Map attributes = Map.of(ZenidConst.SDK_INIT_TOKEN, "sdk-init-token"); + Map attributes = Map.of("sdk-init-token", UUID.randomUUID().toString()); VerificationSdkInfo verificationSdkInfo = provider.initVerificationSdk(ownerId, attributes); - assertNotNull(verificationSdkInfo.getAttributes().get(ZenidConst.SDK_INIT_RESPONSE), "Missing SDK init response"); + assertNotNull(verificationSdkInfo.getAttributes().get("zenid-sdk-init-response"), "Missing SDK init response"); } private void cleanupDocuments(OwnerId ownerId) throws Exception { - if (uploadIds.size() > 0) { + if (!uploadIds.isEmpty()) { provider.cleanupDocuments(ownerId, uploadIds); } } @@ -215,7 +213,7 @@ private List createSubmittedDocuments() throws Exception { private SubmittedDocument createIdCardFrontDocument() throws IOException { SubmittedDocument idCardFront = new SubmittedDocument(); idCardFront.setDocumentId(DOC_ID_CARD_FRONT); - Image idCardFrontPhoto = TestUtil.loadPhoto("/images/specimen_id_front.jpg"); + Image idCardFrontPhoto = loadPhoto("/images/specimen_id_front.jpg"); idCardFront.setPhoto(idCardFrontPhoto); idCardFront.setSide(CardSide.FRONT); idCardFront.setType(DocumentType.ID_CARD); @@ -226,7 +224,7 @@ private SubmittedDocument createIdCardFrontDocument() throws IOException { private SubmittedDocument createIdCardBackDocument() throws IOException { SubmittedDocument idCardBack = new SubmittedDocument(); idCardBack.setDocumentId(DOC_ID_CARD_BACK); - Image idCardBackPhoto = TestUtil.loadPhoto("/images/specimen_id_back.jpg"); + Image idCardBackPhoto = loadPhoto("/images/specimen_id_back.jpg"); idCardBack.setPhoto(idCardBackPhoto); idCardBack.setSide(CardSide.BACK); idCardBack.setType(DocumentType.ID_CARD); @@ -241,4 +239,41 @@ private OwnerId createOwnerId() { return ownerId; } + private static Image loadPhoto(final String path) throws IOException { + final File file = new File(path); + + return Image.builder() + .data(readImageData(path)) + .filename(file.getName()) + .build(); + } + + private static byte[] readImageData(final String path) throws IOException { + try (InputStream stream = ZenidDocumentVerificationProviderTest.class.getResourceAsStream(path)) { + if (stream == null) { + throw new IllegalStateException("Unable to get a stream for: " + path); + } + return stream.readAllBytes(); + } + } + + private static void assertSubmittedDocuments(OwnerId ownerId, List documents, DocumentsSubmitResult result) { + assertEquals(documents.size(), result.getResults().size(), "Different size of submitted documents than expected"); + assertNotNull(result.getExtractedPhotoId(), "Missing extracted photoId"); + + final List submittedDocsIds = result.getResults().stream() + .map(DocumentSubmitResult::getDocumentId) + .toList(); + assertEquals(documents.size(), submittedDocsIds.size(), "Different size of unique submitted documents than expected"); + documents.forEach(document -> + assertTrue(submittedDocsIds.contains(document.getDocumentId()))); + + result.getResults().forEach(submitResult -> { + assertNull(submitResult.getErrorDetail()); + assertNull(submitResult.getRejectReason()); + + assertNotNull(submitResult.getUploadId()); + }); + } + } diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/service/ZenidRestApiServiceTest.java b/enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiServiceTest.java similarity index 98% rename from enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/service/ZenidRestApiServiceTest.java rename to enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiServiceTest.java index 781c4425a..e2ee8f064 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/zenid/service/ZenidRestApiServiceTest.java +++ b/enrollment-server-onboarding-provider-zenid/src/test/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiServiceTest.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -package com.wultra.app.onboardingserver.docverify.zenid.service; +package com.wultra.app.onboardingserver.provider.zenid; -import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; -import com.wultra.app.onboardingserver.docverify.zenid.model.api.ZenidWebInvestigateResponse; +import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; +import com.wultra.app.onboardingserver.provider.zenid.model.api.ZenidWebInvestigateResponse; import lombok.extern.slf4j.Slf4j; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.test.context.ActiveProfiles; @@ -51,6 +52,7 @@ }) @ActiveProfiles("test") @Slf4j +@MockBean(DocumentVerificationRepository.class) class ZenidRestApiServiceTest { // TODO (racansky, 2023-05-18) find the way how to set the same random port for mock server and property diff --git a/enrollment-server-onboarding-provider-zenid/src/test/resources/application-external-service.properties b/enrollment-server-onboarding-provider-zenid/src/test/resources/application-external-service.properties new file mode 100644 index 000000000..dee2e49f5 --- /dev/null +++ b/enrollment-server-onboarding-provider-zenid/src/test/resources/application-external-service.properties @@ -0,0 +1,7 @@ +enrollment-server-onboarding.document-verification.provider=zenid + +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.serialization.FAIL_ON_EMPTY_BEANS=false +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.serialization.WRITE_DATES_AS_TIMESTAMPS=false +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.serialization.WRITE_DATES_WITH_ZONE_ID=true +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.deserialization.ADJUST_DATES_TO_CONTEXT_TIME_ZONE=false +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.deserialization.FAIL_ON_UNKNOWN_PROPERTIES=false diff --git a/enrollment-server-onboarding-provider-zenid/src/test/resources/application-test.properties b/enrollment-server-onboarding-provider-zenid/src/test/resources/application-test.properties new file mode 100644 index 000000000..a9e613c15 --- /dev/null +++ b/enrollment-server-onboarding-provider-zenid/src/test/resources/application-test.properties @@ -0,0 +1,5 @@ +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.serialization.FAIL_ON_EMPTY_BEANS=false +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.serialization.WRITE_DATES_AS_TIMESTAMPS=false +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.serialization.WRITE_DATES_WITH_ZONE_ID=true +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.deserialization.ADJUST_DATES_TO_CONTEXT_TIME_ZONE=false +enrollment-server-onboarding.document-verification.zenid.restClientConfig.jacksonConfiguration.deserialization.FAIL_ON_UNKNOWN_PROPERTIES=false \ No newline at end of file diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index 324935d90..040c7ebfd 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -40,10 +40,20 @@ enrollment-server-onboarding-domain-model ${project.version} + com.wultra.security - enrollment-server-onboarding-common - ${project.version} + enrollment-server-onboarding-api + + + + com.wultra.security + enrollment-server-onboarding-provider-iproov + + + + com.wultra.security + enrollment-server-onboarding-provider-zenid @@ -85,10 +95,6 @@ org.springframework.boot spring-boot-starter-validation - - org.springframework.boot - spring-boot-starter-oauth2-client - jakarta.servlet @@ -196,66 +202,6 @@
    - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - external-service - - - - org.openapitools - openapi-generator-maven-plugin - ${openapi-generator-maven-plugin.version} - - - swagger-definitions-iproov - - generate - - - false - ${basedir}/src/main/resources/api/api-iproov.json - java - false - false - true - false - false - true - - native - true - com.wultra.app.onboardingserver.presencecheck.iproov.model.api - true - - - - - swagger-definitions-zenid - - generate - - - false - ${basedir}/src/main/resources/api/api-zenid.yaml - java - false - false - true - false - false - - native - true - com.wultra.app.onboardingserver.docverify.zenid.model.api - true - - - - - org.apache.maven.plugins maven-war-plugin diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java index 094de983d..f5cf4700c 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java @@ -21,8 +21,8 @@ import com.wultra.app.enrollmentserver.api.model.onboarding.response.*; import com.wultra.app.onboardingserver.common.errorhandling.*; import com.wultra.app.onboardingserver.errorhandling.DocumentSubmitException; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.impl.service.IdentityVerificationRestService; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java index 3cae75e39..539e9b36f 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java @@ -26,8 +26,8 @@ import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.docverify.mock.MockConst; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.provider.DocumentVerificationProvider; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/DefaultExceptionHandler.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/DefaultExceptionHandler.java index b34285375..94b07ebfd 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/DefaultExceptionHandler.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/errorhandling/DefaultExceptionHandler.java @@ -18,6 +18,8 @@ package com.wultra.app.onboardingserver.errorhandling; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.common.errorhandling.*; import io.getlime.core.rest.model.base.response.ErrorResponse; import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthAuthenticationException; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/DataExtractionService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/DataExtractionService.java index a40cdcb1b..843341e2c 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/DataExtractionService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/DataExtractionService.java @@ -17,7 +17,7 @@ */ package com.wultra.app.onboardingserver.impl.service; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.enrollmentserver.model.Document; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java index 3869683d0..961cd7886 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java @@ -30,8 +30,8 @@ import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.configuration.OnboardingConfig; import com.wultra.app.onboardingserver.errorhandling.DocumentSubmitException; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.impl.service.document.DocumentProcessingService; import com.wultra.app.onboardingserver.impl.service.validation.OnboardingConsentApprovalRequestValidator; import com.wultra.app.onboardingserver.impl.service.validation.OnboardingConsentTextRequestValidator; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationService.java index 7c02d6c1e..f4c8c2d8e 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationService.java @@ -38,11 +38,11 @@ import com.wultra.app.onboardingserver.common.service.OnboardingProcessLimitService; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.errorhandling.DocumentSubmitException; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.errorhandling.IdentityVerificationNotFoundException; import com.wultra.app.onboardingserver.impl.service.document.DocumentProcessingService; import com.wultra.app.onboardingserver.impl.service.verification.VerificationProcessingService; -import com.wultra.app.onboardingserver.provider.DocumentVerificationProvider; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import com.wultra.app.onboardingserver.statemachine.guard.document.RequiredDocumentTypesCheck; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ImageProcessor.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ImageProcessor.java index 5b8252206..2b4db63ed 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ImageProcessor.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ImageProcessor.java @@ -20,7 +20,7 @@ import com.google.common.io.Files; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; -import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java index f72931cf6..814e8fd7a 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java @@ -31,12 +31,12 @@ import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; import com.wultra.app.onboardingserver.common.service.AuditService; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.errorhandling.PresenceCheckLimitException; import com.wultra.app.onboardingserver.impl.service.document.DocumentProcessingService; import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService; -import com.wultra.app.onboardingserver.provider.PresenceCheckProvider; +import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java index 4d0e2b736..5cd96d745 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java @@ -32,9 +32,9 @@ import com.wultra.app.onboardingserver.common.service.CommonOnboardingService; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.errorhandling.DocumentSubmitException; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.impl.service.DataExtractionService; -import com.wultra.app.onboardingserver.provider.DocumentVerificationProvider; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentVerificationService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentVerificationService.java index a979d0476..7c4220432 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentVerificationService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentVerificationService.java @@ -32,9 +32,9 @@ import com.wultra.app.onboardingserver.common.service.AuditService; import com.wultra.app.onboardingserver.common.service.CommonOnboardingService; import com.wultra.app.onboardingserver.common.service.OnboardingProcessLimitService; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService; -import com.wultra.app.onboardingserver.provider.DocumentVerificationProvider; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java index 71b5e2761..fb6282b5f 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java @@ -30,9 +30,9 @@ import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; import com.wultra.app.onboardingserver.common.service.AuditService; import com.wultra.app.onboardingserver.common.service.CommonOnboardingService; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService; -import com.wultra.app.onboardingserver.provider.DocumentVerificationProvider; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingService.java index d3455a432..233c558a9 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingService.java @@ -24,7 +24,7 @@ import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.ErrorDetail; import com.wultra.app.onboardingserver.common.service.AuditService; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.enrollmentserver.model.integration.DocumentVerificationResult; import com.wultra.app.enrollmentserver.model.integration.DocumentsVerificationResult; import com.wultra.app.enrollmentserver.model.integration.OwnerId; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java index 5d69b3a62..797d5212d 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java @@ -22,7 +22,7 @@ import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult; import com.wultra.app.enrollmentserver.model.integration.SessionInfo; -import com.wultra.app.onboardingserver.provider.PresenceCheckProvider; +import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckInitAction.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckInitAction.java index c720f0116..e8ab93771 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckInitAction.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckInitAction.java @@ -19,6 +19,8 @@ import com.wultra.app.enrollmentserver.api.model.onboarding.response.PresenceCheckInitResponse; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.SessionInfo; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; import com.wultra.app.onboardingserver.common.errorhandling.OnboardingProcessLimitException; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckVerificationAction.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckVerificationAction.java index b2d59b197..9ace45692 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckVerificationAction.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/presencecheck/PresenceCheckVerificationAction.java @@ -21,7 +21,7 @@ import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService; import com.wultra.app.onboardingserver.impl.service.PresenceCheckService; import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/DocumentVerificationFinalAction.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/DocumentVerificationFinalAction.java index 3e23c0e0c..d0bc37b3d 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/DocumentVerificationFinalAction.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/DocumentVerificationFinalAction.java @@ -20,7 +20,7 @@ import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.OnboardingProcessException; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.impl.service.document.DocumentVerificationService; import com.wultra.app.onboardingserver.statemachine.consts.EventHeaderName; import com.wultra.app.onboardingserver.statemachine.consts.ExtendedStateVariable; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/VerificationDocumentStartAction.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/VerificationDocumentStartAction.java index 5f4d2911a..a2ef6f2cf 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/VerificationDocumentStartAction.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/action/verification/VerificationDocumentStartAction.java @@ -19,7 +19,7 @@ import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService; import com.wultra.app.onboardingserver.statemachine.consts.EventHeaderName; import com.wultra.app.onboardingserver.statemachine.consts.ExtendedStateVariable; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/interceptor/CustomStateMachineInterceptor.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/interceptor/CustomStateMachineInterceptor.java index b302e2c42..5ee7fc3f0 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/interceptor/CustomStateMachineInterceptor.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/interceptor/CustomStateMachineInterceptor.java @@ -16,6 +16,8 @@ */ package com.wultra.app.onboardingserver.statemachine.interceptor; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; import com.wultra.app.onboardingserver.common.errorhandling.OnboardingProcessException; diff --git a/enrollment-server-onboarding/src/main/resources/api/api-zenid.json b/enrollment-server-onboarding/src/main/resources/api/api-zenid.json deleted file mode 100644 index d850bfefb..000000000 --- a/enrollment-server-onboarding/src/main/resources/api/api-zenid.json +++ /dev/null @@ -1,2961 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "version": "v1", - "title": "ZenidWeb" - }, - "host": "raiffeisen.frauds.zenid.cz", - "schemes": [ - "https" - ], - "paths": { - "/api/sample": { - "post": { - "tags": [ - "Api" - ], - "summary": "This method uploads a sample for processing. Sample can be a single image or video file. This call gets file, normalize it (e.t. rotate etc) and OCR its content.\r\nThe file content can be sent in two different formats:\r\nRAW: send the file content as body of the request in binary form without alternations\r\nFORM: use the multipart form encoding and send the file content in file variable\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,\r\nEmptyBody - no uploaded file.", - "operationId": "Api_UploadSample", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "expectedSampleType", - "in": "query", - "description": "Expected type of sample. Set this if you know the type of sample ahead to speed up processing. Required.", - "required": true, - "type": "string", - "enum": [ - "Unknown", - "Selfie", - "DocumentPicture", - "SelfieVideo", - "DocumentVideo", - "Archived" - ] - }, - { - "name": "uploadSessionID", - "in": "query", - "description": "SessionID is GUID created by client to group multiple sample uploads together", - "required": false, - "type": "string", - "format": "uuid" - }, - { - "name": "customData", - "in": "query", - "description": "Custom data to be associated with this sample. Sample can later be located using customData. Any string.", - "required": false, - "type": "string" - }, - { - "name": "fileName", - "in": "query", - "description": "Name of the original file (for example DSC01.jpg). If form upload is needed, then this is optional", - "required": false, - "type": "string" - }, - { - "name": "country", - "in": "query", - "description": "Expected country of the uploaded document (Cz, At, Sk...)", - "required": false, - "type": "string", - "enum": [ - "Cz", - "Sk", - "At", - "Hu", - "Pl", - "De", - "Hr", - "Ro", - "Ru", - "Ua", - "It", - "Dk", - "Es", - "Fi", - "Fr", - "Gb", - "Is", - "Nl", - "Se", - "Si", - "Bg", - "Be", - "Ee", - "Ie", - "Cy", - "Lt", - "Lv", - "Lu", - "Mt", - "Pt", - "Gr" - ] - }, - { - "name": "role", - "in": "query", - "description": "Expected role of the uploaded document (IDC, Passport, Drivers licence)", - "required": false, - "type": "string", - "enum": [ - "Idc", - "Pas", - "Drv", - "Res", - "Gun", - "Hic", - "Std", - "Car", - "Birth", - "Add", - "Ide" - ] - }, - { - "name": "fileLastWriteTime", - "in": "query", - "description": "Last write time of the file uploaded. This can be determined using javascript. Value is used for fraud detection (EXIF comparison)", - "required": false, - "type": "string", - "format": "date-time" - }, - { - "name": "async", - "in": "query", - "description": "Set this true if you want this request to respond as soon as the data are uploaded for processing, not waiting for result.", - "required": false, - "type": "boolean" - }, - { - "name": "callbackUrl", - "in": "query", - "description": "Set URL for call back here, if you want to receive JSON object with response there.", - "required": false, - "type": "string" - }, - { - "name": "searchForSubsamples", - "in": "query", - "description": "For videos, this makes ZenID extract static picture of the card from the video of card. For document pictures, if single sample contains multiple card pictures (for example multi-page PDF or scan with multiple picture), set this to true to search for them and extract as separate \"subsamples\"", - "required": false, - "type": "boolean" - }, - { - "name": "anonymizeImage", - "in": "query", - "description": "ZenID can optionally anonymize specific documents by blackening certain fields. Set this to true to perform anonymization", - "required": false, - "type": "boolean" - }, - { - "name": "documentCode", - "in": "query", - "description": "Code of the specific document if you know it", - "required": false, - "type": "string", - "enum": [ - "IDC2", - "DRV", - "IDC1", - "PAS", - "SK_IDC_2008plus", - "SK_DRV_2004_08_09", - "SK_DRV_2013", - "SK_DRV_2015", - "SK_PAS_2008_14", - "SK_DRV_1993", - "PL_IDC_2015", - "DE_IDC_2010", - "DE_IDC_2001", - "HR_IDC_2013_15", - "AT_IDE_2000", - "HU_IDC_2000_01_12", - "HU_IDC_2016", - "AT_IDC_2002_05_10", - "HU_ADD_2012", - "AT_PAS_2006_14", - "AT_DRV_2006", - "AT_DRV_2013", - "CZ_RES_2011_14", - "CZ_RES_2006_T", - "CZ_RES_2006_07", - "CZ_GUN_2014", - "HU_PAS_2006_12", - "HU_DRV_2012_13", - "HU_DRV_2012_B", - "EU_EHIC_2004_A", - "Unknown", - "CZ_GUN_2017", - "CZ_RES_2020", - "PL_IDC_2019", - "IT_PAS_2006_10", - "INT_ISIC_2008", - "DE_PAS", - "DK_PAS", - "ES_PAS", - "FI_PAS", - "FR_PAS", - "GB_PAS", - "IS_PAS", - "NL_PAS", - "RO_PAS", - "SE_PAS", - "PL_PAS", - "PL_DRV_2013", - "CZ_BIRTH", - "CZ_VEHICLE_I", - "INT_ISIC_2019", - "SI_PAS", - "SI_IDC", - "SI_DRV", - "EU_EHIC_2004_B", - "PL_IDC_2001_02_13", - "IT_IDC_2016", - "HR_PAS_2009_15", - "HR_DRV_2013", - "HR_IDC_2003", - "SI_DRV_2009", - "BG_PAS_2010", - "BG_IDC_2010", - "BG_DRV_2010_13", - "HR_IDC_2021", - "AT_IDC_2021", - "DE_PAS_2007", - "DE_DRV_2013_21", - "DE_DRV_1999_01_04_11", - "FR_IDC_2021", - "FR_IDC_1988_94", - "ES_PAS_2003_06", - "ES_IDC_2015", - "ES_IDC_2006", - "IT_IDC_2004", - "RO_IDC_2001_06_09_17_21", - "NL_IDC_2014_17_21", - "BE_PAS_2014_17_19", - "BE_IDC_2013_15", - "BE_IDC_2020_21", - "GR_PAS_2020", - "PT_PAS_2006_09", - "PT_PAS_2017", - "PT_IDC_2007_08_09_15", - "SE_IDC_2012_21", - "FI_IDC_2017_21", - "IE_PAS_2006_13", - "LT_PAS_2008_09_11_19", - "LT_IDC_2009_12", - "LV_PAS_2015", - "LV_PAS_2007", - "LV_IDC_2012", - "LV_IDC_2019", - "EE_PAS_2014", - "EE_PAS_2021", - "EE_IDC_2011", - "EE_IDC_2018_21", - "CY_PAS_2010_20", - "CY_IDC_2000_08", - "CY_IDC_2015_20", - "LU_PAS_2015", - "LU_IDC_2014_21", - "LU_IDC_2008_13", - "MT_PAS_2008", - "MT_IDC_2014", - "PL_PAS_2011", - "PL_DRV_1999", - "LT_IDC_2021" - ] - }, - { - "name": "pageCode", - "in": "query", - "description": "Side of the document to seek if you know it", - "required": false, - "type": "string", - "enum": [ - "F", - "B" - ] - }, - { - "name": "priorityQueueName", - "in": "query", - "description": "Setting this puts the request into a separate queue for processing, ignoring standard queue.", - "required": false, - "type": "string" - }, - { - "name": "profile", - "in": "query", - "description": "Optional name of profile. Use it to sort samples by different input channels for example.", - "required": false, - "type": "string" - }, - { - "name": "processingMode", - "in": "query", - "description": "Fast (default) or Slow. Slow process mode might fix some alignment problems but is slower.", - "required": false, - "type": "string", - "enum": [ - "Fast", - "Slow" - ] - }, - { - "name": "sdkSignature", - "in": "query", - "description": "Optional SDK signature for pictures. Generated by SDK", - "required": false, - "type": "string" - }, - { - "name": "File", - "in": "formData", - "description": "Upload samples file", - "required": true, - "type": "file" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.UploadSampleResponse" - } - } - } - } - }, - "/api/investigateSamples": { - "get": { - "tags": [ - "Api" - ], - "summary": "Investigation node. Investigation gets list of samples, combine mined data from them in one combined object (see MinedAllData), and validate it with set of validators.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,\r\nUnknownSampleID - any of sampleIDs is not stored in DB.\r\nSampleInInvalidState - sample is in invalid state - for example it is waiting for operators or processing of sample ended with error\r\nInvalidSampleCombination - investigation must contain samples from single person/customer", - "operationId": "Api_InvestigateSamples", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "sampleIDs", - "in": "query", - "description": "List of strings - sample IDs. Required.", - "required": true, - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi" - }, - { - "name": "profile", - "in": "query", - "description": "Optional name of profile. Each profile defines settings for validators.", - "required": false, - "type": "string" - }, - { - "name": "customData", - "in": "query", - "description": "Custom data to be associated with this sample. Sample can later be located using customData. Any string.", - "required": false, - "type": "string" - }, - { - "name": "async", - "in": "query", - "description": "Set this true if you want this request to respond as soon as the data are uploaded for processing, not waiting for result.", - "required": false, - "type": "boolean" - }, - { - "name": "callbackUrl", - "in": "query", - "description": "Set URL for call back here, if you want to receive JSON object with response there.", - "required": false, - "type": "string" - }, - { - "name": "language", - "in": "query", - "description": "Language of the output (Czech/English).", - "required": false, - "type": "string", - "enum": [ - "English", - "Czech", - "Polish", - "German" - ] - }, - { - "name": "priorityQueueName", - "in": "query", - "description": "Setting this puts the request into a separate queue for processing, ignoring standard queue.", - "required": false, - "type": "string" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.InvestigateResponse" - } - } - } - } - }, - "/api/investigateUploadSession": { - "get": { - "tags": [ - "Api" - ], - "summary": "Investigation node. Investigation gets GUID, unique identifier, gets all samples tagged with this UploadSessionID, combine mined data from them in one combined object (see MinedAllData), and validate it with set of validators.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,\r\nUnknownUploadSessionID - UploadSessionID is not known\r\nSampleInInvalidState - sample is in invalid state - for example it is waiting for operators or processing of sample ended with error\r\nInvalidSampleCombination - investigation must contain samples from single person/customer", - "operationId": "Api_InvestigateUploadSession", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "uploadSessionID", - "in": "query", - "description": "User identification of list of samples. Required.", - "required": true, - "type": "string", - "format": "uuid" - }, - { - "name": "profile", - "in": "query", - "description": "Optional name of profile. Each profile defines settings for validators.", - "required": false, - "type": "string" - }, - { - "name": "customData", - "in": "query", - "description": "Custom data to be associated with this sample. Sample can later be located using customData. Any string.", - "required": false, - "type": "string" - }, - { - "name": "async", - "in": "query", - "description": "Set this true if you want this request to respond as soon as the data are uploaded for processing, not waiting for result.", - "required": false, - "type": "boolean" - }, - { - "name": "callbackUrl", - "in": "query", - "description": "Set URL for call back here, if you want to receive JSON object with response there.", - "required": false, - "type": "string" - }, - { - "name": "language", - "in": "query", - "description": "Language of the output (Czech/English).", - "required": false, - "type": "string", - "enum": [ - "English", - "Czech", - "Polish", - "German" - ] - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.InvestigateResponse" - } - } - } - } - }, - "/api/sample/{sampleID}": { - "get": { - "tags": [ - "Api" - ], - "summary": "Node for synchronizing samples - returns sample for given ID. This call can be used for synchronizing information about samples with external systems.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,\r\nUnknownSampleID - if that ID is not used in DB for any sample.", - "operationId": "Api_GetSample", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "sampleID", - "in": "path", - "description": "ID of the sample", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.UploadSampleResponse" - } - } - } - } - }, - "/api/deletePerson": { - "get": { - "tags": [ - "Api" - ], - "summary": "Deletes all information related to a person", - "operationId": "Api_DeletePerson", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "sampleId", - "in": "query", - "description": "ID of the sample to start person search", - "required": false, - "type": "string" - }, - { - "name": "cardIdentifier", - "in": "query", - "description": "ID of the card for which we search the related documents", - "required": false, - "type": "string" - }, - { - "name": "firstName", - "in": "query", - "description": "First name of the person whose documents are searched for", - "required": false, - "type": "string" - }, - { - "name": "lastName", - "in": "query", - "description": "Surname of the person whose documents are searched for", - "required": false, - "type": "string" - }, - { - "name": "birthNumber", - "in": "query", - "description": "Birth Number (RČ in czech) of the person whose documents are searched for", - "required": false, - "type": "string" - }, - { - "name": "birthDate", - "in": "query", - "description": "Date of birth of the person whose documents are searched for", - "required": false, - "type": "string", - "format": "date-time" - }, - { - "name": "deleteType", - "in": "query", - "description": "Set if you want delete only samples and investigations or face from FaceDB or everything", - "required": false, - "type": "string", - "enum": [ - "Everything", - "FacesOnly", - "SamplesAndInvestigationsOnly" - ] - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.DeletePersonResponse" - } - } - } - } - }, - "/api/deleteSample": { - "get": { - "tags": [ - "Api" - ], - "summary": "Deletes a sample. Also deletes an investigation in which this sample was used", - "operationId": "Api_DeleteSample", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "sampleId", - "in": "query", - "description": "ID of the sample to delete", - "required": false, - "type": "string" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.DeleteSampleResponse" - } - } - } - } - }, - "/api/investigation/{investigationID}": { - "get": { - "tags": [ - "Api" - ], - "summary": "Node for synchronizing investigations - returns investigations for given ID. This call can be used for synchronizing information about samples with external systems.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,\r\nUnknownSampleID - if Investigation ID is not stored in DB.", - "operationId": "Api_GetInvestigation", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "investigationID", - "in": "path", - "description": "ID of the investigation", - "required": true, - "type": "integer", - "format": "int32" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.InvestigateResponse" - } - } - } - } - }, - "/api/samples": { - "get": { - "tags": [ - "Api" - ], - "summary": "Get list of samples (newer than timestamp). This call can be used for synchronizing information about samples with external systems.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,\r\nInvalidTimeStamp - error while decoding timestamp.", - "operationId": "Api_GetSamples", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "timestamp", - "in": "query", - "description": "if defined, list is limited to samples newer than given timestamp", - "required": false, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.ListSamplesResponse" - } - } - } - } - }, - "/api/investigations": { - "get": { - "tags": [ - "Api" - ], - "summary": "Get list of investigations (newer than timestamp). This call can be used for synchronizing information about samples with external systems.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,\r\nInvalidTimeStamp - error while decoding timestamp.", - "operationId": "Api_GetInvestigations", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "timestamp", - "in": "query", - "description": "if defined, list is limited to investigations newer than given timestamp", - "required": false, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.ListInvestigationsResponse" - } - } - } - } - }, - "/api/profiles": { - "get": { - "tags": [ - "Api" - ], - "summary": "Get list of names of profiles, defined in system. This call can be used for selecting profile in investigation.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,", - "operationId": "Api_GetProfiles", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.ListProfilesResponse" - } - } - } - } - }, - "/api/validators": { - "get": { - "tags": [ - "Api" - ], - "summary": "Returns list of validators - their id and text description.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these): InternalServerError - problem while preprocessing or unknown problem while processing,", - "operationId": "Api_GetValidatorEnum", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "language", - "in": "query", - "description": "Optional parameter for defining output language (Czech, German).", - "required": false, - "type": "string", - "enum": [ - "English", - "Czech", - "Polish", - "German" - ] - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - } - } - }, - "/api/diagnostics": { - "get": { - "tags": [ - "Api" - ], - "summary": "Performs diagnostics test. Note depending on parameters, license might be consumed", - "operationId": "Api_Diagnostics", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "checkSelfie", - "in": "query", - "description": "Checks face API is working - THIS CAUSES LICENSE USAGE", - "required": false, - "type": "boolean" - }, - { - "name": "checkCloud", - "in": "query", - "description": "Checks cloud services are working - THIS CAUSES LICENSE USAGE", - "required": false, - "type": "boolean" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.DiagnosticsResponse" - } - } - } - } - }, - "/api/initSdk": { - "get": { - "tags": [ - "Api" - ], - "summary": "Returns string required for a correct function of mobile and Web SDK.", - "operationId": "Api_InitSdk", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "token", - "in": "query", - "description": "Challenge token generated by SDK function.", - "required": false, - "type": "string" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.InitSdkResponse" - } - } - } - } - }, - "/api/face": { - "post": { - "tags": [ - "Api" - ], - "summary": "Loads face image in the image repository (for comparing faces for validation).\r\nIn most cases, /api/sample with sampleType=Selfie should be used instead.\r\nWhile processing, error could be identified.\r\nIn that case empty response with described ErrorCode and Description is returned (one of these):\r\nInternalServerError - problem while preprocessing or unknown problem while processing or disabled face database.", - "operationId": "Api_UploadFace", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "birthNumber", - "in": "query", - "description": "Birth namber, required for image/face coupling.", - "required": true, - "type": "string" - }, - { - "name": "fileName", - "in": "query", - "description": "Name of the input file", - "required": false, - "type": "string" - }, - { - "name": "async", - "in": "query", - "description": "Set this true if you want this request to respond as soon as the data are uploaded for processing, not waiting for result.", - "required": false, - "type": "boolean" - }, - { - "name": "callbackUrl", - "in": "query", - "description": "Optional, used if api/face called asynchroniously", - "required": false, - "type": "string" - }, - { - "name": "File", - "in": "formData", - "description": "Upload face", - "required": true, - "type": "file" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.UploadFaceResponse" - } - } - } - } - }, - "/api/verifyCardsRecalled": { - "post": { - "tags": [ - "Api" - ], - "summary": "Verifies card validity using same means as existing CardRecalled validator\r\nIt takes a List of cards to verify - each with DocumentCode and card number and returns same list with Recalled status. Recalled status can be either True = recalled, False = not recalled, Null = could not determine/not supported document code.", - "operationId": "Api_VerifyCardsRecalled", - "consumes": [ - "application/json", - "text/json", - "application/xml", - "text/xml", - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json", - "text/json", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "request", - "in": "body", - "description": "List of cards to verify - each with DocumentCode and card number", - "required": true, - "schema": { - "$ref": "#/definitions/ZenidWeb.VerifyCardsRecalledRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/ZenidWeb.VerifyCardsRecalledResponse" - } - } - } - } - } - }, - "definitions": { - "ZenidWeb.UploadSampleResponse": { - "description": "Response object for UploadSample", - "type": "object", - "properties": { - "SampleID": { - "description": "Unique ID of the sample in ZenID system.", - "type": "string" - }, - "CustomData": { - "description": "Copy of the input parameter CustomData", - "type": "string" - }, - "UploadSessionID": { - "format": "uuid", - "description": "Copy of the input parameter UploadSessionID", - "type": "string", - "example": "00000000-0000-0000-0000-000000000000" - }, - "SampleType": { - "description": "Real SampleType", - "enum": [ - "Unknown", - "Selfie", - "DocumentPicture", - "SelfieVideo", - "DocumentVideo", - "Archived" - ], - "type": "string" - }, - "MinedData": { - "$ref": "#/definitions/ZenidShared.MineAllResult", - "description": "Structure of data, mined from sample - {ZenidShared.MineAllResult}." - }, - "State": { - "description": "State of the request - NotDone/Done/Error", - "enum": [ - "NotDone", - "Done", - "Error", - "Operator", - "Rejected" - ], - "type": "string" - }, - "ProjectedImage": { - "$ref": "#/definitions/ZenidShared.Hash", - "description": "hash of the source projected image" - }, - "ParentSampleID": { - "description": "hash of the parent sampleID if this is a subsample", - "type": "string" - }, - "AnonymizedImage": { - "$ref": "#/definitions/ZenidShared.Hash", - "description": "Hash of the censored projected image" - }, - "ImageUrlFormat": { - "description": "link to the source projected image", - "type": "string" - }, - "ImagePageCount": { - "format": "int32", - "description": "Number of pages this document has (in case of PDF or TIFF). This can be used in history URL /history/image/{hash}?page=1", - "type": "integer" - }, - "Subsamples": { - "description": "If subsample processing is enable, this list contains further images extracted from the primary image, each with extra document image", - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.UploadSampleResponse" - } - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidShared.MineAllResult": { - "type": "object", - "properties": { - "FirstName": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "LastName": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "Address": { - "$ref": "#/definitions/ZenidShared.MinedAddress" - }, - "BirthAddress": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "BirthLastName": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "BirthNumber": { - "$ref": "#/definitions/ZenidShared.MinedRc" - }, - "BirthDate": { - "$ref": "#/definitions/ZenidShared.MinedDate" - }, - "ExpiryDate": { - "$ref": "#/definitions/ZenidShared.MinedDate" - }, - "IssueDate": { - "$ref": "#/definitions/ZenidShared.MinedDate" - }, - "IdcardNumber": { - "$ref": "#/definitions/ZenidShared.MinedText", - "description": "identification number for id card - set only on id cards" - }, - "DrivinglicenseNumber": { - "$ref": "#/definitions/ZenidShared.MinedText", - "description": "identification number for driving licence - set only on driving licences" - }, - "PassportNumber": { - "$ref": "#/definitions/ZenidShared.MinedText", - "description": "identification number for passport - set only on passports" - }, - "Sex": { - "$ref": "#/definitions/ZenidShared.MinedSex" - }, - "Nationality": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "Authority": { - "$ref": "#/definitions/ZenidShared.MinedText", - "description": "Authority (state agency) issued this document" - }, - "MaritalStatus": { - "$ref": "#/definitions/ZenidShared.MinedMaritalStatus" - }, - "Photo": { - "$ref": "#/definitions/ZenidShared.MinedPhoto" - }, - "Mrz": { - "$ref": "#/definitions/ZenidShared.MinedMrz", - "description": "Machine readable zone" - }, - "DocumentCode": { - "description": "Code identificating document (when combining from more samples the most probable version is set)", - "enum": [ - "IDC2", - "DRV", - "IDC1", - "PAS", - "SK_IDC_2008plus", - "SK_DRV_2004_08_09", - "SK_DRV_2013", - "SK_DRV_2015", - "SK_PAS_2008_14", - "SK_DRV_1993", - "PL_IDC_2015", - "DE_IDC_2010", - "DE_IDC_2001", - "HR_IDC_2013_15", - "AT_IDE_2000", - "HU_IDC_2000_01_12", - "HU_IDC_2016", - "AT_IDC_2002_05_10", - "HU_ADD_2012", - "AT_PAS_2006_14", - "AT_DRV_2006", - "AT_DRV_2013", - "CZ_RES_2011_14", - "CZ_RES_2006_T", - "CZ_RES_2006_07", - "CZ_GUN_2014", - "HU_PAS_2006_12", - "HU_DRV_2012_13", - "HU_DRV_2012_B", - "EU_EHIC_2004_A", - "Unknown", - "CZ_GUN_2017", - "CZ_RES_2020", - "PL_IDC_2019", - "IT_PAS_2006_10", - "INT_ISIC_2008", - "DE_PAS", - "DK_PAS", - "ES_PAS", - "FI_PAS", - "FR_PAS", - "GB_PAS", - "IS_PAS", - "NL_PAS", - "RO_PAS", - "SE_PAS", - "PL_PAS", - "PL_DRV_2013", - "CZ_BIRTH", - "CZ_VEHICLE_I", - "INT_ISIC_2019", - "SI_PAS", - "SI_IDC", - "SI_DRV", - "EU_EHIC_2004_B", - "PL_IDC_2001_02_13", - "IT_IDC_2016", - "HR_PAS_2009_15", - "HR_DRV_2013", - "HR_IDC_2003", - "SI_DRV_2009", - "BG_PAS_2010", - "BG_IDC_2010", - "BG_DRV_2010_13", - "HR_IDC_2021", - "AT_IDC_2021", - "DE_PAS_2007", - "DE_DRV_2013_21", - "DE_DRV_1999_01_04_11", - "FR_IDC_2021", - "FR_IDC_1988_94", - "ES_PAS_2003_06", - "ES_IDC_2015", - "ES_IDC_2006", - "IT_IDC_2004", - "RO_IDC_2001_06_09_17_21", - "NL_IDC_2014_17_21", - "BE_PAS_2014_17_19", - "BE_IDC_2013_15", - "BE_IDC_2020_21", - "GR_PAS_2020", - "PT_PAS_2006_09", - "PT_PAS_2017", - "PT_IDC_2007_08_09_15", - "SE_IDC_2012_21", - "FI_IDC_2017_21", - "IE_PAS_2006_13", - "LT_PAS_2008_09_11_19", - "LT_IDC_2009_12", - "LV_PAS_2015", - "LV_PAS_2007", - "LV_IDC_2012", - "LV_IDC_2019", - "EE_PAS_2014", - "EE_PAS_2021", - "EE_IDC_2011", - "EE_IDC_2018_21", - "CY_PAS_2010_20", - "CY_IDC_2000_08", - "CY_IDC_2015_20", - "LU_PAS_2015", - "LU_IDC_2014_21", - "LU_IDC_2008_13", - "MT_PAS_2008", - "MT_IDC_2014", - "PL_PAS_2011", - "PL_DRV_1999", - "LT_IDC_2021" - ], - "type": "string" - }, - "DocumentCountry": { - "description": "Country associated with this document type", - "enum": [ - "Cz", - "Sk", - "At", - "Hu", - "Pl", - "De", - "Hr", - "Ro", - "Ru", - "Ua", - "It", - "Dk", - "Es", - "Fi", - "Fr", - "Gb", - "Is", - "Nl", - "Se", - "Si", - "Bg", - "Be", - "Ee", - "Ie", - "Cy", - "Lt", - "Lv", - "Lu", - "Mt", - "Pt", - "Gr" - ], - "type": "string" - }, - "DocumentRole": { - "description": "General role of this document (ID card vs Passport vs Driver license etc)", - "enum": [ - "Idc", - "Pas", - "Drv", - "Res", - "Gun", - "Hic", - "Std", - "Car", - "Birth", - "Add", - "Ide" - ], - "type": "string" - }, - "PageCode": { - "description": "identification of page of document", - "enum": [ - "F", - "B" - ], - "type": "string" - }, - "Height": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "EyesColor": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "CarNumber": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "FirstNameOfParents": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "ResidencyNumber": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "ResidencyNumberPhoto": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "ResidencyPermitDescription": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "ResidencyPermitCode": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "GunlicenseNumber": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "Titles": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "TitlesAfter": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "SpecialRemarks": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "MothersName": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "HealthInsuranceCardNumber": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "InsuranceCompanyCode": { - "$ref": "#/definitions/ZenidShared.MinedText" - }, - "IssuingCountry": { - "$ref": "#/definitions/ZenidShared.MinedText" - } - } - }, - "ZenidShared.Hash": { - "description": "Simple MD5 hash wrapper with easy compare/text conversions", - "type": "object", - "properties": { - "AsText": { - "type": "string" - }, - "IsNull": { - "type": "boolean", - "readOnly": true - } - } - }, - "ZenidShared.MinedText": { - "description": "Identifies mined text - its value and confidence", - "type": "object", - "properties": { - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.MinedAddress": { - "type": "object", - "properties": { - "ID": { - "type": "string" - }, - "A1": { - "description": "physical first row of address on card", - "type": "string" - }, - "A2": { - "description": "physical second row of address on card", - "type": "string" - }, - "A3": { - "description": "physical third row of address on card", - "type": "string" - }, - "AdministrativeAreaLevel1": { - "description": "main admin. area - in CZ - kraj", - "type": "string" - }, - "AdministrativeAreaLevel2": { - "description": "secondary admin. area - in CZ - okres or towns behaves also as okres - like Brno", - "type": "string" - }, - "Locality": { - "description": "identification of town/city/village (if not already defined up - Brno, Praha) / OSM: boundary=administrative+ admin_level=8", - "type": "string" - }, - "Sublocality": { - "description": "town-subdivision\r\nCZ - čtvrť/katastrální území (Neighborhood/Cadastral place) / OSM: boundary=administrative+ admin_level=10\r\nSK - čtvrť/katastrální území (Neighborhood/Cadastral place) / OSM: boundary=administrative+ admin_level=10\r\nDE - stadtteil without selfgovernment / OSM: boundary=administrative+ admin_level=10\r\nHU - admin-level 9\r\n \r\ntodo slovak: Valaská - Piesok is in addess, but Piesok is just place=village, no admin_level=10", - "type": "string" - }, - "Suburb": { - "description": "town-subdivision - selfgoverning - probably used only in CZ and maybe DE\r\nCZ - městská část/obvod / OSM: addr:suburb - it can be in multiple cadastral places (parts cadastral place Trnitá is in suburb Brno-střed and Brno-jih)\r\nDE - stadtteil without selfgovernment / OSM: boundary=administrative+ admin_level=9\r\n \r\ntodo not used outside CZ right now, so it is not searched/mined from osm, just ruian", - "type": "string" - }, - "Street": { - "description": "in CZ - ulice", - "type": "string" - }, - "HouseNumber": { - "description": "descriptive house number in town - used in Czechia, Slovakia, Austria (číslo popisné, číslo súpisné, Konskriptionsnummer)", - "type": "string" - }, - "StreetNumber": { - "description": "descriptive number of house on the street - in CZ - číslo orientační", - "type": "string" - }, - "PostalCode": { - "description": "in CZ - poštovní směrovací číslo - PSČ", - "type": "string" - }, - "GoogleSearchable": { - "type": "string", - "readOnly": true - }, - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.MinedRc": { - "description": "Object containing mined information about birth-number - checksum, date, sex...", - "type": "object", - "properties": { - "BirthDate": { - "format": "date-time", - "description": "Date of the birth - can be parsed from RC identifier", - "type": "string" - }, - "Checksum": { - "format": "int32", - "type": "integer", - "readOnly": true - }, - "Sex": { - "enum": [ - "F", - "M" - ], - "type": "string" - }, - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.MinedDate": { - "description": "object for storing Mined Date - Date, default Format, Test and Confidence.", - "type": "object", - "properties": { - "Date": { - "format": "date-time", - "type": "string" - }, - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.MinedSex": { - "description": "MinedSex - test of field, its confidence and property Sex (parsed text)", - "type": "object", - "properties": { - "Sex": { - "enum": [ - "F", - "M" - ], - "type": "string" - }, - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.MinedMaritalStatus": { - "description": "MinedMaritalStatus - test of field, its confidence and property MaritalStatus (parsed text)", - "type": "object", - "properties": { - "MaritalStatus": { - "enum": [ - "Single", - "Married", - "Divorced", - "Widowed", - "Partnership" - ], - "type": "string" - }, - "ImpliedSex": { - "enum": [ - "F", - "M" - ], - "type": "string" - }, - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.MinedPhoto": { - "description": "MinedPhoto - shows image data, and also two face-related values - estimated age and sex.", - "type": "object", - "properties": { - "ImageData": { - "$ref": "#/definitions/ZenidShared.LazyMatImage" - }, - "EstimatedAge": { - "format": "double", - "type": "number" - }, - "EstimatedSex": { - "enum": [ - "F", - "M" - ], - "type": "string" - }, - "HasOccludedMouth": { - "type": "boolean" - }, - "HasSunGlasses": { - "type": "boolean" - }, - "HasHeadWear": { - "type": "boolean" - }, - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.MinedMrz": { - "description": "Declare mined Text, Confidence, and also structure Mrz.", - "type": "object", - "properties": { - "Mrz": { - "$ref": "#/definitions/ZenidShared.Mrz" - }, - "Text": { - "type": "string" - }, - "Confidence": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidShared.LazyMatImage": { - "type": "object", - "properties": { - "ImageHash": { - "$ref": "#/definitions/ZenidShared.Hash" - } - } - }, - "ZenidShared.Mrz": { - "type": "object", - "properties": { - "Type": { - "enum": [ - "ID_v2000", - "ID_v2012", - "PAS_v2006", - "Unknown", - "AUT_IDC2002", - "AUT_PAS2006", - "SVK_IDC2008", - "SVK_DL2013", - "SVK_PAS2008", - "POL_IDC2015", - "HRV_IDC2003", - "CZE_RES_2011_14", - "HUN_PAS_2006_12", - "HU_IDC_2000_01_12_16" - ], - "type": "string" - }, - "Subtype": { - "enum": [ - "OP", - "R", - "D", - "S", - "Default", - "Unknown" - ], - "type": "string" - }, - "BirthDate": { - "description": "Inner Birth date string of MRZ. Low-level data, ignore it. Use BirthDate from MineAllResult object.", - "type": "string" - }, - "BirthDateVerified": { - "description": "Inner flag, if MRZ BirthDate checksum is ok. Low-level check, ignore it. Use Validators.", - "type": "boolean" - }, - "DocumentNumber": { - "description": "Inner Document number string of MRZ. Low-level data, ignore it. Use value from MineAllResult object.", - "type": "string" - }, - "DocumentNumberVerified": { - "description": "Inner flag, if MRZ DocumentNumber checksum is ok. Low-level check, ignore it. Use Validators.", - "type": "boolean" - }, - "ExpiryDate": { - "description": "Inner Expiry date string of MRZ. Low-level data, ignore it. Use value from MineAllResult object.", - "type": "string" - }, - "ExpiryDateVerified": { - "description": "Inner flag, if MRZ ExpiryDate checksum is ok. Low-level check, ignore it. Use Validators.", - "type": "boolean" - }, - "GivenName": { - "description": "Inner Given name string of MRZ. Low-level data, ignore it. Use value from MineAllResult object.", - "type": "string" - }, - "ChecksumVerified": { - "description": "Inner flag, if checksum of MRZ itself is ok. Low-level check, ignore it. Use Validators.", - "type": "boolean" - }, - "ChecksumDigit": { - "format": "int32", - "description": "Inner value of global MRZ checksum.", - "type": "integer" - }, - "LastName": { - "description": "Inner Last name string of MRZ. Low-level data, ignore it. Use value from MineAllResult object.", - "type": "string" - }, - "Nationality": { - "description": "Inner Nationality string of MRZ. Low-level data, ignore it. Use value from MineAllResult object.", - "type": "string" - }, - "Sex": { - "description": "Inner Sex string of MRZ. Low-level data, ignore it. Use value from MineAllResult object.", - "type": "string" - }, - "BirthNumber": { - "description": "Inner Birthnumber string of MRZ (used on Czech passports). Low-level data, ignore it. Use value from MineAllResult object.", - "type": "string" - }, - "BirthNumberChecksum": { - "format": "int32", - "description": "Inner value of Birthnumber checksum in MRZ (on Czech passports). Low-level check, ignore it. Use Validators.", - "type": "integer" - }, - "BirthNumberVerified": { - "description": "Inner flag, if MRZ BirthNumber checksum is ok (used on Czech passports). Low-level check, ignore it. Use Validators.", - "type": "boolean" - }, - "BirthdateChecksum": { - "format": "int32", - "description": "Inner value of MRZ BirthDate checksum.", - "type": "integer" - }, - "DocumentNumChecksum": { - "format": "int32", - "description": "Inner value of MRZ DocumentNumber checksum.", - "type": "integer" - }, - "ExpiryChecksum": { - "format": "int32", - "description": "Inner value of MRZ ExpiryDate checksum.", - "type": "integer" - }, - "IssueDate": { - "description": "Prefix of the MRZ (type of the MRZ + subtype (differs, some ID cards have ID, other I_ or IO) + country issuer. Low-level data, can be ignored.", - "type": "string" - }, - "IssueDateParsed": { - "format": "date-time", - "type": "string", - "readOnly": true - }, - "AdditionalData": { - "description": "Output of OptionalSubstructure dont fitting in IssueDate or BirthNumber", - "type": "string" - }, - "BirthDateParsed": { - "format": "date-time", - "description": "Inner machine-readable value of BirthDate (in DateTime structure). Low-level data, use value from MineAllResult object.", - "type": "string", - "readOnly": true - }, - "ExpiryDateParsed": { - "format": "date-time", - "description": "Inner machine-readable value of ExpiryDate (in DateTime structure). Low-level data, use value from MineAllResult object.", - "type": "string", - "readOnly": true - }, - "MrzLength": { - "$ref": "#/definitions/System.ValueTuple[System.Int32,System.Int32]", - "readOnly": true - }, - "MrzDefType": { - "enum": [ - "TD1_IDC", - "TD2_IDC2000", - "TD3_PAS", - "SKDRV", - "None" - ], - "type": "string" - } - } - }, - "System.ValueTuple[System.Int32,System.Int32]": { - "type": "object", - "properties": { - "Item1": { - "format": "int32", - "type": "integer" - }, - "Item2": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidWeb.InvestigateResponse": { - "description": "Response object for the investigation nodes.", - "type": "object", - "properties": { - "InvestigationID": { - "format": "int32", - "description": "Unique identification of the investigation (set of samples)", - "type": "integer" - }, - "CustomData": { - "description": "Copy of the input parameter CustomData", - "type": "string" - }, - "MinedData": { - "$ref": "#/definitions/ZenidShared.MineAllResult", - "description": "Structure of data, mined from sample - {ZenidShared.MineAllResult}." - }, - "DocumentsData": { - "description": "If investigation covers multiple documents, each will have their own entry here", - "type": "array", - "items": { - "$ref": "#/definitions/ZenidShared.MineAllResult" - } - }, - "InvestigationUrl": { - "description": "URL of the investigation detail", - "type": "string" - }, - "ValidatorResults": { - "description": "Result of the all validators - List of {ZenidWeb.InvestigationValidatorResponse}", - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.InvestigationValidatorResponse" - } - }, - "State": { - "description": "State of the request - NotDone/Done/Error", - "enum": [ - "NotDone", - "Done", - "Error", - "Operator", - "Rejected" - ], - "type": "string" - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.InvestigationValidatorResponse": { - "type": "object", - "properties": { - "Name": { - "type": "string" - }, - "Code": { - "format": "int32", - "description": "Code identification of validator in external system", - "type": "integer" - }, - "Score": { - "format": "int32", - "description": "Score of the validator for given input", - "type": "integer" - }, - "AcceptScore": { - "format": "int32", - "description": "Accept score - if score is higher than accept score, Validator response OK is set to true", - "type": "integer" - }, - "Issues": { - "description": "Description of the issues of validation (why score is lower)", - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.InvestigationIssueResponse" - } - }, - "Ok": { - "type": "boolean" - } - } - }, - "ZenidWeb.InvestigationIssueResponse": { - "type": "object", - "properties": { - "IssueUrl": { - "description": "Url with detailed visualization of the issue.", - "type": "string" - }, - "IssueDescription": { - "description": "Description of issue", - "type": "string" - }, - "DocumentCode": { - "description": "Document code of sample, where issue is present", - "enum": [ - "IDC2", - "DRV", - "IDC1", - "PAS", - "SK_IDC_2008plus", - "SK_DRV_2004_08_09", - "SK_DRV_2013", - "SK_DRV_2015", - "SK_PAS_2008_14", - "SK_DRV_1993", - "PL_IDC_2015", - "DE_IDC_2010", - "DE_IDC_2001", - "HR_IDC_2013_15", - "AT_IDE_2000", - "HU_IDC_2000_01_12", - "HU_IDC_2016", - "AT_IDC_2002_05_10", - "HU_ADD_2012", - "AT_PAS_2006_14", - "AT_DRV_2006", - "AT_DRV_2013", - "CZ_RES_2011_14", - "CZ_RES_2006_T", - "CZ_RES_2006_07", - "CZ_GUN_2014", - "HU_PAS_2006_12", - "HU_DRV_2012_13", - "HU_DRV_2012_B", - "EU_EHIC_2004_A", - "Unknown", - "CZ_GUN_2017", - "CZ_RES_2020", - "PL_IDC_2019", - "IT_PAS_2006_10", - "INT_ISIC_2008", - "DE_PAS", - "DK_PAS", - "ES_PAS", - "FI_PAS", - "FR_PAS", - "GB_PAS", - "IS_PAS", - "NL_PAS", - "RO_PAS", - "SE_PAS", - "PL_PAS", - "PL_DRV_2013", - "CZ_BIRTH", - "CZ_VEHICLE_I", - "INT_ISIC_2019", - "SI_PAS", - "SI_IDC", - "SI_DRV", - "EU_EHIC_2004_B", - "PL_IDC_2001_02_13", - "IT_IDC_2016", - "HR_PAS_2009_15", - "HR_DRV_2013", - "HR_IDC_2003", - "SI_DRV_2009", - "BG_PAS_2010", - "BG_IDC_2010", - "BG_DRV_2010_13", - "HR_IDC_2021", - "AT_IDC_2021", - "DE_PAS_2007", - "DE_DRV_2013_21", - "DE_DRV_1999_01_04_11", - "FR_IDC_2021", - "FR_IDC_1988_94", - "ES_PAS_2003_06", - "ES_IDC_2015", - "ES_IDC_2006", - "IT_IDC_2004", - "RO_IDC_2001_06_09_17_21", - "NL_IDC_2014_17_21", - "BE_PAS_2014_17_19", - "BE_IDC_2013_15", - "BE_IDC_2020_21", - "GR_PAS_2020", - "PT_PAS_2006_09", - "PT_PAS_2017", - "PT_IDC_2007_08_09_15", - "SE_IDC_2012_21", - "FI_IDC_2017_21", - "IE_PAS_2006_13", - "LT_PAS_2008_09_11_19", - "LT_IDC_2009_12", - "LV_PAS_2015", - "LV_PAS_2007", - "LV_IDC_2012", - "LV_IDC_2019", - "EE_PAS_2014", - "EE_PAS_2021", - "EE_IDC_2011", - "EE_IDC_2018_21", - "CY_PAS_2010_20", - "CY_IDC_2000_08", - "CY_IDC_2015_20", - "LU_PAS_2015", - "LU_IDC_2014_21", - "LU_IDC_2008_13", - "MT_PAS_2008", - "MT_IDC_2014", - "PL_PAS_2011", - "PL_DRV_1999", - "LT_IDC_2021" - ], - "type": "string" - }, - "FieldID": { - "description": "FieldID wher issue is present", - "enum": [ - "A1", - "A2", - "A3", - "FirstName", - "LastName", - "Photo", - "BirthDate", - "BirthNumber", - "Authority", - "Mrz1", - "Mrz2", - "Mrz3", - "IdcardNumber", - "Sex", - "MaritalStatus", - "BirthAddress", - "BA1", - "BA2", - "IssueDate", - "ExpiryDate", - "PassportNumber", - "DrivinglicenseNumber", - "Barcode", - "BirthLastName", - "SpecialRemarks", - "Height", - "EyesColor", - "Titles", - "Authority1", - "Authority2", - "LastName1", - "LastName2TitlesAfter", - "DrvCodes", - "Signature", - "OtherInfo", - "MiniHologram", - "MiniPhoto", - "CarNumber", - "LicenseTypes", - "FirstNameOfParents", - "BirthDateNumber", - "DrivinglicenseNumber2", - "RDIFChipAccess", - "Pseudonym", - "ResidencyPermitDescription", - "ResidencyPermitCode", - "ResidencyNumber", - "AuthorityAndIssueDate", - "Nationality", - "GunlicenseNumber", - "Stamp", - "Stamp2", - "SurnameAndName1", - "SurnameAndName2", - "SurnameAndName3", - "MothersSurnameAndName", - "TemporaryAddress1", - "TemporaryAddress2", - "AddressStartingDate", - "TemporaryAddressStartingDate", - "TemporaryAddressEndingDate", - "NameInNationalLanguage", - "BirthDateAndAddress", - "SpecialRemarks2", - "SpecialRemarks3", - "Unknown", - "HealthInsuranceCardNumber", - "InsuranceCompanyCode", - "IssuingCountry", - "ResidencyNumberPhoto", - "IssueDateAndAuthority", - "TitlesAfter", - "PlaceOfIssue", - "BirthAddressAndDate", - "IssueDateAndPlaceOfIssue", - "MothersSurname", - "MothersName", - "FathersSurname", - "FathersName", - "LastName2", - "A4", - "FirstName2", - "IssueAndExpiryDate", - "FiscalNumber", - "SocialNumber", - "AlternativeName" - ], - "type": "string" - }, - "SampleID": { - "description": "ID of the identification issue", - "type": "string" - }, - "PageCode": { - "description": "Identification of the page type for issue", - "enum": [ - "F", - "B" - ], - "type": "string" - }, - "SampleType": { - "description": "Type of sample", - "enum": [ - "Unknown", - "Selfie", - "DocumentPicture", - "SelfieVideo", - "DocumentVideo", - "Archived" - ], - "type": "string" - } - } - }, - "ZenidWeb.DeletePersonResponse": { - "type": "object", - "properties": { - "DeletedSampleIDs": { - "type": "array", - "items": { - "type": "string" - } - }, - "DeletedFacesFromSampleIDs": { - "type": "array", - "items": { - "type": "string" - } - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.DeleteSampleResponse": { - "type": "object", - "properties": { - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.ListSamplesResponse": { - "description": "Return value of api/samples", - "type": "object", - "properties": { - "Results": { - "description": "List of declarations of samples - ID, CustomData, UploadSessionID, State", - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.ListSamplesResponse.SampleItem" - } - }, - "TimeStamp": { - "format": "int64", - "description": "Timestamp limit (if defined as input)", - "type": "integer" - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.ListSamplesResponse.SampleItem": { - "type": "object", - "properties": { - "SampleID": { - "description": "DB ID of given sample.", - "type": "string" - }, - "ParentSampleID": { - "description": "If the sample is subsample image created from primary one, this is the ID of primary image", - "type": "string" - }, - "CustomData": { - "description": "CustomData attribute (copied from Request)", - "type": "string" - }, - "UploadSessionID": { - "format": "uuid", - "description": "GUID of upload session set.", - "type": "string", - "example": "00000000-0000-0000-0000-000000000000" - }, - "State": { - "description": "State of the investigation", - "enum": [ - "NotDone", - "Done", - "Error", - "Operator", - "Rejected" - ], - "type": "string" - } - } - }, - "ZenidWeb.ListInvestigationsResponse": { - "description": "Return value of api/investigation", - "type": "object", - "properties": { - "Results": { - "description": "List of declarations of samples - ID, CustpmData, State", - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.ListInvestigationsResponse.InvestigateItem" - } - }, - "TimeStamp": { - "format": "int64", - "description": "Timestamp limit (if defined as input)", - "type": "integer" - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.ListInvestigationsResponse.InvestigateItem": { - "description": "Short description of investigation (its ID, State and CUstomData)", - "type": "object", - "properties": { - "InvestigationID": { - "format": "int32", - "description": "DB ID of investigation", - "type": "integer" - }, - "CustomData": { - "description": "CustomData attribute (copied from Request)", - "type": "string" - }, - "State": { - "description": "State of the investigation", - "enum": [ - "NotDone", - "Done", - "Error", - "Operator", - "Rejected" - ], - "type": "string" - } - } - }, - "ZenidWeb.ListProfilesResponse": { - "description": "Return value of api/profiles", - "type": "object", - "properties": { - "Results": { - "description": "List of names of profiles", - "type": "array", - "items": { - "type": "string" - } - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.DiagnosticsResponse": { - "description": "Response object for UploadSample", - "type": "object", - "properties": { - "IsAllOk": { - "type": "boolean" - }, - "SelfCheckItems": { - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.Controllers.SelfCheck.SelfCheckItem" - } - }, - "LicenseExpiration": { - "format": "date-time", - "type": "string" - }, - "LicenseRemaining": { - "$ref": "#/definitions/ZenidShared.LicenseCountables" - }, - "SupportedDocuments": { - "type": "array", - "items": { - "enum": [ - "IDC2", - "DRV", - "IDC1", - "PAS", - "SK_IDC_2008plus", - "SK_DRV_2004_08_09", - "SK_DRV_2013", - "SK_DRV_2015", - "SK_PAS_2008_14", - "SK_DRV_1993", - "PL_IDC_2015", - "DE_IDC_2010", - "DE_IDC_2001", - "HR_IDC_2013_15", - "AT_IDE_2000", - "HU_IDC_2000_01_12", - "HU_IDC_2016", - "AT_IDC_2002_05_10", - "HU_ADD_2012", - "AT_PAS_2006_14", - "AT_DRV_2006", - "AT_DRV_2013", - "CZ_RES_2011_14", - "CZ_RES_2006_T", - "CZ_RES_2006_07", - "CZ_GUN_2014", - "HU_PAS_2006_12", - "HU_DRV_2012_13", - "HU_DRV_2012_B", - "EU_EHIC_2004_A", - "Unknown", - "CZ_GUN_2017", - "CZ_RES_2020", - "PL_IDC_2019", - "IT_PAS_2006_10", - "INT_ISIC_2008", - "DE_PAS", - "DK_PAS", - "ES_PAS", - "FI_PAS", - "FR_PAS", - "GB_PAS", - "IS_PAS", - "NL_PAS", - "RO_PAS", - "SE_PAS", - "PL_PAS", - "PL_DRV_2013", - "CZ_BIRTH", - "CZ_VEHICLE_I", - "INT_ISIC_2019", - "SI_PAS", - "SI_IDC", - "SI_DRV", - "EU_EHIC_2004_B", - "PL_IDC_2001_02_13", - "IT_IDC_2016", - "HR_PAS_2009_15", - "HR_DRV_2013", - "HR_IDC_2003", - "SI_DRV_2009", - "BG_PAS_2010", - "BG_IDC_2010", - "BG_DRV_2010_13", - "HR_IDC_2021", - "AT_IDC_2021", - "DE_PAS_2007", - "DE_DRV_2013_21", - "DE_DRV_1999_01_04_11", - "FR_IDC_2021", - "FR_IDC_1988_94", - "ES_PAS_2003_06", - "ES_IDC_2015", - "ES_IDC_2006", - "IT_IDC_2004", - "RO_IDC_2001_06_09_17_21", - "NL_IDC_2014_17_21", - "BE_PAS_2014_17_19", - "BE_IDC_2013_15", - "BE_IDC_2020_21", - "GR_PAS_2020", - "PT_PAS_2006_09", - "PT_PAS_2017", - "PT_IDC_2007_08_09_15", - "SE_IDC_2012_21", - "FI_IDC_2017_21", - "IE_PAS_2006_13", - "LT_PAS_2008_09_11_19", - "LT_IDC_2009_12", - "LV_PAS_2015", - "LV_PAS_2007", - "LV_IDC_2012", - "LV_IDC_2019", - "EE_PAS_2014", - "EE_PAS_2021", - "EE_IDC_2011", - "EE_IDC_2018_21", - "CY_PAS_2010_20", - "CY_IDC_2000_08", - "CY_IDC_2015_20", - "LU_PAS_2015", - "LU_IDC_2014_21", - "LU_IDC_2008_13", - "MT_PAS_2008", - "MT_IDC_2014", - "PL_PAS_2011", - "PL_DRV_1999", - "LT_IDC_2021" - ], - "type": "string" - } - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.Controllers.SelfCheck.SelfCheckItem": { - "type": "object", - "properties": { - "Name": { - "type": "string" - }, - "Status": { - "type": "boolean" - }, - "Comment": { - "type": "string" - } - } - }, - "ZenidShared.LicenseCountables": { - "type": "object", - "properties": { - "PageCount": { - "format": "int32", - "description": "Note this is actually \"document count\"", - "type": "integer" - }, - "SelfieCount": { - "format": "int32", - "type": "integer" - }, - "FraudCount": { - "format": "int32", - "type": "integer" - } - } - }, - "ZenidWeb.InitSdkResponse": { - "type": "object", - "properties": { - "Response": { - "type": "string" - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.UploadFaceResponse": { - "description": "Return object for /api/face", - "type": "object", - "properties": { - "UploadFaceResult": { - "description": "Possibly result of the upload face photo", - "enum": [ - "Ok", - "FaceNotDetected", - "ImageExistsWithDifferentCustomerData" - ], - "type": "string" - }, - "OriginalImageHash": { - "description": "hash of the original image", - "type": "string" - }, - "PersistedFace": { - "format": "uuid", - "description": "GUID - link of the face image in the Oxford API repository", - "type": "string", - "example": "00000000-0000-0000-0000-000000000000" - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.VerifyCardsRecalledRequest": { - "type": "object", - "properties": { - "CardsToVerify": { - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.VerifyCardsRecalledRequest.CardInfo" - } - } - } - }, - "ZenidWeb.VerifyCardsRecalledRequest.CardInfo": { - "type": "object", - "properties": { - "DocumentCode": { - "enum": [ - "IDC2", - "DRV", - "IDC1", - "PAS", - "SK_IDC_2008plus", - "SK_DRV_2004_08_09", - "SK_DRV_2013", - "SK_DRV_2015", - "SK_PAS_2008_14", - "SK_DRV_1993", - "PL_IDC_2015", - "DE_IDC_2010", - "DE_IDC_2001", - "HR_IDC_2013_15", - "AT_IDE_2000", - "HU_IDC_2000_01_12", - "HU_IDC_2016", - "AT_IDC_2002_05_10", - "HU_ADD_2012", - "AT_PAS_2006_14", - "AT_DRV_2006", - "AT_DRV_2013", - "CZ_RES_2011_14", - "CZ_RES_2006_T", - "CZ_RES_2006_07", - "CZ_GUN_2014", - "HU_PAS_2006_12", - "HU_DRV_2012_13", - "HU_DRV_2012_B", - "EU_EHIC_2004_A", - "Unknown", - "CZ_GUN_2017", - "CZ_RES_2020", - "PL_IDC_2019", - "IT_PAS_2006_10", - "INT_ISIC_2008", - "DE_PAS", - "DK_PAS", - "ES_PAS", - "FI_PAS", - "FR_PAS", - "GB_PAS", - "IS_PAS", - "NL_PAS", - "RO_PAS", - "SE_PAS", - "PL_PAS", - "PL_DRV_2013", - "CZ_BIRTH", - "CZ_VEHICLE_I", - "INT_ISIC_2019", - "SI_PAS", - "SI_IDC", - "SI_DRV", - "EU_EHIC_2004_B", - "PL_IDC_2001_02_13", - "IT_IDC_2016", - "HR_PAS_2009_15", - "HR_DRV_2013", - "HR_IDC_2003", - "SI_DRV_2009", - "BG_PAS_2010", - "BG_IDC_2010", - "BG_DRV_2010_13", - "HR_IDC_2021", - "AT_IDC_2021", - "DE_PAS_2007", - "DE_DRV_2013_21", - "DE_DRV_1999_01_04_11", - "FR_IDC_2021", - "FR_IDC_1988_94", - "ES_PAS_2003_06", - "ES_IDC_2015", - "ES_IDC_2006", - "IT_IDC_2004", - "RO_IDC_2001_06_09_17_21", - "NL_IDC_2014_17_21", - "BE_PAS_2014_17_19", - "BE_IDC_2013_15", - "BE_IDC_2020_21", - "GR_PAS_2020", - "PT_PAS_2006_09", - "PT_PAS_2017", - "PT_IDC_2007_08_09_15", - "SE_IDC_2012_21", - "FI_IDC_2017_21", - "IE_PAS_2006_13", - "LT_PAS_2008_09_11_19", - "LT_IDC_2009_12", - "LV_PAS_2015", - "LV_PAS_2007", - "LV_IDC_2012", - "LV_IDC_2019", - "EE_PAS_2014", - "EE_PAS_2021", - "EE_IDC_2011", - "EE_IDC_2018_21", - "CY_PAS_2010_20", - "CY_IDC_2000_08", - "CY_IDC_2015_20", - "LU_PAS_2015", - "LU_IDC_2014_21", - "LU_IDC_2008_13", - "MT_PAS_2008", - "MT_IDC_2014", - "PL_PAS_2011", - "PL_DRV_1999", - "LT_IDC_2021" - ], - "type": "string" - }, - "CardNumber": { - "type": "string" - } - } - }, - "ZenidWeb.VerifyCardsRecalledResponse": { - "type": "object", - "properties": { - "VerifiedCards": { - "type": "array", - "items": { - "$ref": "#/definitions/ZenidWeb.VerifyCardsRecalledResponse.VerifiedCard" - } - }, - "ErrorCode": { - "description": "If throght processing some error occurs, ErrorCode property is set.", - "enum": [ - "UnknownSampleID", - "UnknownUploadSessionID", - "EmptyBody", - "InternalServerError", - "InvalidTimeStamp", - "SampleInInvalidState", - "InvalidSampleCombination", - "AccessDenied", - "UnknownPerson", - "InvalidInputData" - ], - "type": "string" - }, - "ErrorText": { - "description": "Error text", - "type": "string" - }, - "MessageType": { - "type": "string", - "readOnly": true - } - } - }, - "ZenidWeb.VerifyCardsRecalledResponse.VerifiedCard": { - "type": "object", - "properties": { - "Recalled": { - "type": "boolean" - }, - "DocumentCode": { - "enum": [ - "IDC2", - "DRV", - "IDC1", - "PAS", - "SK_IDC_2008plus", - "SK_DRV_2004_08_09", - "SK_DRV_2013", - "SK_DRV_2015", - "SK_PAS_2008_14", - "SK_DRV_1993", - "PL_IDC_2015", - "DE_IDC_2010", - "DE_IDC_2001", - "HR_IDC_2013_15", - "AT_IDE_2000", - "HU_IDC_2000_01_12", - "HU_IDC_2016", - "AT_IDC_2002_05_10", - "HU_ADD_2012", - "AT_PAS_2006_14", - "AT_DRV_2006", - "AT_DRV_2013", - "CZ_RES_2011_14", - "CZ_RES_2006_T", - "CZ_RES_2006_07", - "CZ_GUN_2014", - "HU_PAS_2006_12", - "HU_DRV_2012_13", - "HU_DRV_2012_B", - "EU_EHIC_2004_A", - "Unknown", - "CZ_GUN_2017", - "CZ_RES_2020", - "PL_IDC_2019", - "IT_PAS_2006_10", - "INT_ISIC_2008", - "DE_PAS", - "DK_PAS", - "ES_PAS", - "FI_PAS", - "FR_PAS", - "GB_PAS", - "IS_PAS", - "NL_PAS", - "RO_PAS", - "SE_PAS", - "PL_PAS", - "PL_DRV_2013", - "CZ_BIRTH", - "CZ_VEHICLE_I", - "INT_ISIC_2019", - "SI_PAS", - "SI_IDC", - "SI_DRV", - "EU_EHIC_2004_B", - "PL_IDC_2001_02_13", - "IT_IDC_2016", - "HR_PAS_2009_15", - "HR_DRV_2013", - "HR_IDC_2003", - "SI_DRV_2009", - "BG_PAS_2010", - "BG_IDC_2010", - "BG_DRV_2010_13", - "HR_IDC_2021", - "AT_IDC_2021", - "DE_PAS_2007", - "DE_DRV_2013_21", - "DE_DRV_1999_01_04_11", - "FR_IDC_2021", - "FR_IDC_1988_94", - "ES_PAS_2003_06", - "ES_IDC_2015", - "ES_IDC_2006", - "IT_IDC_2004", - "RO_IDC_2001_06_09_17_21", - "NL_IDC_2014_17_21", - "BE_PAS_2014_17_19", - "BE_IDC_2013_15", - "BE_IDC_2020_21", - "GR_PAS_2020", - "PT_PAS_2006_09", - "PT_PAS_2017", - "PT_IDC_2007_08_09_15", - "SE_IDC_2012_21", - "FI_IDC_2017_21", - "IE_PAS_2006_13", - "LT_PAS_2008_09_11_19", - "LT_IDC_2009_12", - "LV_PAS_2015", - "LV_PAS_2007", - "LV_IDC_2012", - "LV_IDC_2019", - "EE_PAS_2014", - "EE_PAS_2021", - "EE_IDC_2011", - "EE_IDC_2018_21", - "CY_PAS_2010_20", - "CY_IDC_2000_08", - "CY_IDC_2015_20", - "LU_PAS_2015", - "LU_IDC_2014_21", - "LU_IDC_2008_13", - "MT_PAS_2008", - "MT_IDC_2014", - "PL_PAS_2011", - "PL_DRV_1999", - "LT_IDC_2021" - ], - "type": "string" - }, - "CardNumber": { - "type": "string" - } - } - } - } -} \ No newline at end of file diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/AbstractDocumentVerificationProviderTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/AbstractDocumentVerificationProviderTest.java deleted file mode 100644 index f5c017202..000000000 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/AbstractDocumentVerificationProviderTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * PowerAuth Enrollment Server - * Copyright (C) 2021 Wultra s.r.o. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.wultra.app.onboardingserver.docverify; - -import com.wultra.app.enrollmentserver.model.integration.DocumentSubmitResult; -import com.wultra.app.enrollmentserver.model.integration.DocumentsSubmitResult; -import com.wultra.app.enrollmentserver.model.integration.OwnerId; -import com.wultra.app.enrollmentserver.model.integration.SubmittedDocument; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * @author Lukas Lukovsky, lukas.lukovsky@wultra.com - */ -public class AbstractDocumentVerificationProviderTest { - - public void assertSubmittedDocuments(OwnerId ownerId, List documents, DocumentsSubmitResult result) { - assertEquals(documents.size(), result.getResults().size(), "Different size of submitted documents than expected"); - assertNotNull(result.getExtractedPhotoId(), "Missing extracted photoId"); - - final List submittedDocsIds = result.getResults().stream() - .map(DocumentSubmitResult::getDocumentId) - .toList(); - assertEquals(documents.size(), submittedDocsIds.size(), "Different size of unique submitted documents than expected"); - documents.forEach(document -> - assertTrue(submittedDocsIds.contains(document.getDocumentId()))); - - result.getResults().forEach(submitResult -> { - assertNull(submitResult.getErrorDetail()); - assertNull(submitResult.getRejectReason()); - - assertNotNull(submitResult.getUploadId()); - }); - } - -} diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java index e2f649ca6..721d72e80 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java @@ -24,7 +24,6 @@ import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; -import com.wultra.app.onboardingserver.docverify.AbstractDocumentVerificationProviderTest; import com.wultra.app.onboardingserver.docverify.mock.MockConst; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -49,7 +48,7 @@ @ActiveProfiles("test") @ComponentScan(basePackages = {"com.wultra.app.onboardingserver.docverify.mock"}) @EnableConfigurationProperties -class WultraMockDocumentVerificationProviderTest extends AbstractDocumentVerificationProviderTest { +class WultraMockDocumentVerificationProviderTest { private WultraMockDocumentVerificationProvider provider; @@ -179,4 +178,23 @@ private SubmittedDocument createSubmittedDocument() { return document; } + private static void assertSubmittedDocuments(OwnerId ownerId, List documents, DocumentsSubmitResult result) { + assertEquals(documents.size(), result.getResults().size(), "Different size of submitted documents than expected"); + assertNotNull(result.getExtractedPhotoId(), "Missing extracted photoId"); + + final List submittedDocsIds = result.getResults().stream() + .map(DocumentSubmitResult::getDocumentId) + .toList(); + assertEquals(documents.size(), submittedDocsIds.size(), "Different size of unique submitted documents than expected"); + documents.forEach(document -> + assertTrue(submittedDocsIds.contains(document.getDocumentId()))); + + result.getResults().forEach(submitResult -> { + assertNull(submitResult.getErrorDetail()); + assertNull(submitResult.getRejectReason()); + + assertNotNull(submitResult.getUploadId()); + }); + } + } diff --git a/enrollment-server-onboarding/src/test/resources/application-external-service.properties b/enrollment-server-onboarding/src/test/resources/application-external-service.properties deleted file mode 100644 index 4b1b5379b..000000000 --- a/enrollment-server-onboarding/src/test/resources/application-external-service.properties +++ /dev/null @@ -1,9 +0,0 @@ -enrollment-server-onboarding.identity-verification.enabled=true - -enrollment-server-onboarding.document-verification.provider=zenid - -enrollment-server-onboarding.document-verification.zenid.asyncProcessingEnabled=false - -enrollment-server-onboarding.presence-check.provider=iproov - -logging.level.root=INFO diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index 11ef15165..1da830ea3 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -182,14 +182,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - ${maven-surefire-plugin.version} - - external-service - - org.apache.maven.plugins maven-war-plugin diff --git a/pom.xml b/pom.xml index 24ca98b4b..3eec7fb00 100644 --- a/pom.xml +++ b/pom.xml @@ -77,10 +77,13 @@ enrollment-server-api-model enrollment-server enrollment-server-onboarding - enrollment-server-onboarding-adapter-mock - enrollment-server-onboarding-domain-model + enrollment-server-onboarding-api enrollment-server-onboarding-api-model + enrollment-server-onboarding-adapter-mock enrollment-server-onboarding-common + enrollment-server-onboarding-domain-model + enrollment-server-onboarding-provider-iproov + enrollment-server-onboarding-provider-zenid @@ -114,7 +117,25 @@ com.wultra.security - enrollment-server-common + enrollment-server-onboarding-common + ${project.version} + + + + com.wultra.security + enrollment-server-onboarding-api + ${project.version} + + + + com.wultra.security + enrollment-server-onboarding-provider-iproov + ${project.version} + + + + com.wultra.security + enrollment-server-onboarding-provider-zenid ${project.version} @@ -244,6 +265,14 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + external-service + + + org.apache.maven.plugins maven-source-plugin From 933e588ed19601c0e8bb22f9e565416bdfbf3c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pe=C5=A1ek?= Date: Thu, 23 Nov 2023 09:16:21 +0100 Subject: [PATCH 21/52] Fix #912: Innovatrics classes generation (#925) --- docs/onboarding/Configuration-Properties.md | 4 +- .../pom.xml | 81 + ...novatricsDocumentVerificationProvider.java | 80 + .../InnovatricsPresenceCheckProvider.java | 58 + .../main/resources/api/api-innovatrics.json | 5653 +++++++++++++++++ .../EnrollmentServerTestApplication.java | 28 + enrollment-server-onboarding/pom.xml | 5 + pom.xml | 9 +- 8 files changed, 5915 insertions(+), 3 deletions(-) create mode 100644 enrollment-server-onboarding-provider-innovatrics/pom.xml create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/resources/api/api-innovatrics.json create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/EnrollmentServerTestApplication.java diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index 214c090b6..25fc6f664 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -74,7 +74,7 @@ The Onboarding Server uses the following public configuration properties: | Property | Default | Note | |---|---|---| -| `enrollment-server-onboarding.document-verification.provider` | `mock` | Document verification provider (`mock`, `zenid`). | +| `enrollment-server-onboarding.document-verification.provider` | `mock` | Document verification provider (`mock`, `zenid`, `innovatrics`). | | `enrollment-server-onboarding.document-verification.cleanupEnabled` | `false` | Whether document cleanup is enabled for the provider. | | `enrollment-server-onboarding.document-verification.checkInProgressDocumentSubmits` | `0/5 * * * * *` | Cron scheduler for checking status of submitted documents. | | `enrollment-server-onboarding.document-verification.checkDocumentsVerifications.cron` | `0/5 * * * * *` | Cron scheduler for checking pending document verifications. | @@ -87,7 +87,7 @@ The Onboarding Server uses the following public configuration properties: | Property | Default | Note | |--------------------------------------------------------------------------------|---------|----------------------------------------------------------------------------------------| | `enrollment-server-onboarding.presence-check.enabled` | `true` | Whether presence check provider is enabled. | -| `enrollment-server-onboarding.presence-check.provider` | `mock` | Presence check provider (`mock`, `iproov`). | +| `enrollment-server-onboarding.presence-check.provider` | `mock` | Presence check provider (`mock`, `iproov`, `innovatrics`). | | `enrollment-server-onboarding.presence-check.cleanupEnabled` | `false` | Whether cleanup of presence check data is enabled. | | `enrollment-server-onboarding.presence-check.verifySelfieWithDocumentsEnabled` | `false` | Whether verification of the presence check selfie photo with the documents is enabled. | | `enrollment-server-onboarding.presence-check.max-failed-attempts` | `5` | Maximum failed attempts for presence check and OTP verification. | diff --git a/enrollment-server-onboarding-provider-innovatrics/pom.xml b/enrollment-server-onboarding-provider-innovatrics/pom.xml new file mode 100644 index 000000000..73dd6c2b0 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + + com.wultra.security + enrollment-server-parent + 1.6.0-SNAPSHOT + + + com.wultra.security + enrollment-server-onboarding-provider-innovatrics + + + + com.wultra.security + enrollment-server-onboarding-api + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.openapitools + openapi-generator-maven-plugin + ${openapi-generator-maven-plugin.version} + + + openapi-definitions-innovatrics + + generate + + + false + ${basedir}/src/main/resources/api/api-innovatrics.json + java + false + false + true + false + false + + native + true + com.wultra.app.onboardingserver.provider.innovatrics.model.api + true + + + + + + + + + \ No newline at end of file diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java new file mode 100644 index 000000000..c7bb40bed --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java @@ -0,0 +1,80 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.app.enrollmentserver.model.integration.*; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; +import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; +import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; +import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; + +/** + * Implementation of the {@link DocumentVerificationProvider} with Innovatrics. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@ConditionalOnProperty(value = "enrollment-server-onboarding.document-verification.provider", havingValue = "innovatrics") +@Component +class InnovatricsDocumentVerificationProvider implements DocumentVerificationProvider { + + @Override + public DocumentsSubmitResult checkDocumentUpload(OwnerId id, DocumentVerificationEntity document) throws RemoteCommunicationException, DocumentVerificationException { + return null; + } + + @Override + public DocumentsSubmitResult submitDocuments(OwnerId id, List documents) throws RemoteCommunicationException, DocumentVerificationException { + return null; + } + + @Override + public DocumentsVerificationResult verifyDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException { + return null; + } + + @Override + public DocumentsVerificationResult getVerificationResult(OwnerId id, String verificationId) throws RemoteCommunicationException, DocumentVerificationException { + return null; + } + + @Override + public Image getPhoto(String photoId) throws RemoteCommunicationException, DocumentVerificationException { + return null; + } + + @Override + public void cleanupDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException { + + } + + @Override + public List parseRejectionReasons(DocumentResultEntity docResult) throws DocumentVerificationException { + return null; + } + + @Override + public VerificationSdkInfo initVerificationSdk(OwnerId id, Map initAttributes) throws RemoteCommunicationException, DocumentVerificationException { + return null; + } +} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java new file mode 100644 index 000000000..1c7c65392 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java @@ -0,0 +1,58 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.app.enrollmentserver.model.integration.Image; +import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult; +import com.wultra.app.enrollmentserver.model.integration.SessionInfo; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; +import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * Implementation of the {@link PresenceCheckProvider} with Innovatrics. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "innovatrics") +@Component +class InnovatricsPresenceCheckProvider implements PresenceCheckProvider { + + @Override + public void initPresenceCheck(OwnerId id, Image photo) throws PresenceCheckException, RemoteCommunicationException { + + } + + @Override + public SessionInfo startPresenceCheck(OwnerId id) throws PresenceCheckException, RemoteCommunicationException { + return null; + } + + @Override + public PresenceCheckResult getResult(OwnerId id, SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException { + return null; + } + + @Override + public void cleanupIdentityData(OwnerId id) throws PresenceCheckException, RemoteCommunicationException { + + } +} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/resources/api/api-innovatrics.json b/enrollment-server-onboarding-provider-innovatrics/src/main/resources/api/api-innovatrics.json new file mode 100644 index 000000000..dea3bf779 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/resources/api/api-innovatrics.json @@ -0,0 +1,5653 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Digital Identity Service API", + "version": "1.25.0" + }, + "servers": [ + { + "url": "https://dot.innovatrics.com/identity" + } + ], + "paths": { + "/api/v1/info": { + "get": { + "tags": [ + "Actuator" + ], + "summary": "Get application info", + "operationId": "info", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActuatorInfo" + } + } + } + } + } + } + }, + "/api/v1/health": { + "get": { + "tags": [ + "Actuator" + ], + "summary": "Get application health", + "operationId": "health", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActuatorHealth" + } + } + } + } + } + } + }, + "/api/v1/customers/{id}/selfie": { + "put": { + "tags": [ + "Customer onboarding" + ], + "summary": "Provide customer\u0027s selfie", + "operationId": "createSelfie_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSelfieRequest" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_BODY\n - INVALID_IMAGE", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSelfieResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + }, + "delete": { + "tags": [ + "Customer onboarding" + ], + "summary": "Delete customer\u0027s selfie", + "operationId": "deleteSelfie", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "204": { + "description": "No Content" + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/liveness": { + "put": { + "tags": [ + "Customer onboarding" + ], + "summary": "Create customer\u0027s liveness", + "operationId": "createLiveness", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomerLivenessResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + }, + "delete": { + "tags": [ + "Customer onboarding" + ], + "summary": "Delete customer\u0027s liveness", + "operationId": "deleteLiveness", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "204": { + "description": "No Content" + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document": { + "put": { + "tags": [ + "Customer onboarding" + ], + "summary": "Create customer\u0027s document", + "operationId": "createDocument", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDocumentRequest" + } + } + }, + "required": true + }, + "responses": { + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDocumentResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_BODY", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + }, + "delete": { + "tags": [ + "Customer onboarding" + ], + "summary": "Delete customer\u0027s document", + "operationId": "deleteDocument", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "204": { + "description": "No Content" + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/pages": { + "put": { + "tags": [ + "Customer onboarding" + ], + "summary": "Provide customer\u0027s document page", + "operationId": "createDocumentPage_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDocumentPageRequest" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_BODY\n - INVALID_IMAGE", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateDocumentPageResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces": { + "post": { + "tags": [ + "Face operations" + ], + "summary": "Create a face from the photo (face detection)", + "operationId": "detect_1", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFaceRequest" + } + } + }, + "required": true + }, + "responses": { + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFaceResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_REQUEST_BODY\n - INVALID_IMAGE\n - FACE_SIZE_MEMORY_LIMIT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{probe_face_id}/similarity": { + "post": { + "tags": [ + "Face operations" + ], + "summary": "Match the probe face to the reference face", + "operationId": "checkSimilarity", + "parameters": [ + { + "name": "probe_face_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceSimilarityRequest" + } + } + }, + "required": true + }, + "responses": { + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceSimilarityResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_REQUEST_BODY\n - INVALID_IMAGE\n - UNSUPPORTED_VERSION_TEMPLATE\n - CORRUPTED_TEMPLATE\n - INCOMPATIBLE_TEMPLATE", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Create a customer", + "operationId": "createCustomer", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomerResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Get the customer", + "operationId": "getCustomer", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCustomerResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + }, + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Create a customer with a custom UUIDv4", + "operationId": "createCustomerWithUuid", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomerResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "422": { + "description": "Unprocessable request\n\nPossible error codes:\n - ALREADY_EXISTS", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + }, + "delete": { + "tags": [ + "Customer onboarding" + ], + "summary": "Delete the customer", + "operationId": "deleteCustomer", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "204": { + "description": "No Content" + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/store": { + "post": { + "tags": [ + "Trust Platform" + ], + "summary": "Store customer in the Trust Platform", + "operationId": "storeInTrustPlatform", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerStoreRequest" + } + } + }, + "required": true + }, + "responses": { + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_BODY", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "204": { + "description": "No Content" + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/liveness/selfies": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Provide customer\u0027s liveness selfie", + "operationId": "createLivenessSelfie", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomerLivenessSelfieRequest" + } + } + }, + "required": true + }, + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_BODY\n - INVALID_IMAGE", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomerLivenessSelfieResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/liveness/records": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Provide customer\u0027s liveness record", + "operationId": "createLivenessRecord", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateCustomerLivenessRecordResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_BODY\n - INVALID_IMAGE", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/liveness/evaluation": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Evaluate customer\u0027s liveness", + "operationId": "evaluateLiveness", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvaluateCustomerLivenessRequest" + } + } + }, + "required": true + }, + "responses": { + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_BODY", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EvaluateCustomerLivenessResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/inspect": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Inspect customer", + "operationId": "inspect", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerInspectResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/inspect/disclose": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Disclose customer inspection", + "operationId": "inspectDisclose", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CustomerInspectDiscloseResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/inspect": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Inspect customer\u0027s document", + "operationId": "documentInspect", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentInspectResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/inspect/disclose": { + "post": { + "tags": [ + "Customer onboarding" + ], + "summary": "Disclose customer\u0027s document inspection", + "operationId": "documentInspectDisclose", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentInspectDiscloseResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "409": { + "description": "Conflict\n\nPossible error codes:\n - CONFLICT", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/metadata": { + "get": { + "tags": [ + "Metadata" + ], + "summary": "Get metadata", + "operationId": "metadata", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentMetadataResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/quality": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Get the face\u0027s quality", + "operationId": "checkQuality", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceQualityResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/glasses": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Check if glasses are present on the face", + "operationId": "checkGlasses", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GlassesResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/face-template": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Get the face\u0027s template", + "operationId": "createTemplate", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceTemplateResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/face-mask": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Check if the face is covered by a mask", + "operationId": "checkFaceMask", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceMaskResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/crop": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Get the face\u0027s crop", + "operationId": "doCrop", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/crop/removed-background": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Get the face\u0027s crop with removed background", + "operationId": "doCropRemoveBackground", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/crop/coordinates": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Get face\u0027s crop coordinates", + "operationId": "doCropCoordinates", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CropCoordinatesResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}/aspects": { + "get": { + "tags": [ + "Face operations" + ], + "summary": "Get face\u0027s aspects", + "operationId": "evaluateAspects", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceAspectsResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/liveness/records/{recordId}/selfie": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Get customer\u0027s liveness record\u0027s selfie", + "operationId": "customerLivenessRecordSelfie", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "recordId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/signature": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Get customer\u0027s document signature", + "operationId": "documentSignature", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/portrait": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Get customer\u0027s document portrait", + "operationId": "documentPortrait", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/pages/{page-type}": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Get normalized image of the customer\u0027s document page", + "operationId": "documentPageCrop", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "page-type", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/pages/{page-type}/quality": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Check quality of the customer\u0027s document page", + "operationId": "documentPageQuality", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "page-type", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DocumentPageQuality" + } + } + } + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/ghost-portrait": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Get customer\u0027s document ghost portrait", + "operationId": "documentGhostPortrait", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/customers/{id}/document/fingerprint": { + "get": { + "tags": [ + "Customer onboarding" + ], + "summary": "Get customer\u0027s fingerprint from the document", + "operationId": "documentFingerprint", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "width", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + }, + { + "name": "height", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImageCrop" + } + } + } + }, + "404": { + "description": "Not found\n\nPossible error codes:\n - NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID\n - INVALID_REQUEST_PARAMETER", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "api": [] + } + ] + } + }, + "/api/v1/faces/{id}": { + "delete": { + "tags": [ + "Face operations" + ], + "summary": "Delete the face", + "operationId": "deleteFace", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Bad request\n\nPossible error codes:\n - INVALID_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden" + }, + "401": { + "description": "Unauthorized" + }, + "500": { + "description": "Internal Server Error\n\nPossible error codes:\n - UNEXPECTED_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "204": { + "description": "No Content" + } + }, + "security": [ + { + "api": [] + } + ] + } + } + }, + "components": { + "schemas": { + "ActuatorInfo": { + "required": [ + "build", + "iface", + "sam" + ], + "type": "object", + "properties": { + "build": { + "required": [ + "artifact", + "group", + "name", + "version" + ], + "type": "object", + "properties": { + "artifact": { + "type": "string", + "readOnly": true, + "example": "digital-identity-service" + }, + "name": { + "type": "string", + "readOnly": true, + "example": "digital-identity-service" + }, + "group": { + "type": "string", + "readOnly": true, + "example": "com.innovatrics.dot" + }, + "version": { + "type": "string", + "readOnly": true, + "example": "1.0.0" + } + }, + "description": "The application build info", + "readOnly": true + }, + "iface": { + "required": [ + "license", + "version" + ], + "type": "object", + "properties": { + "version": { + "type": "string", + "readOnly": true, + "example": "4.18.0" + }, + "license": { + "required": [ + "day", + "month", + "year" + ], + "type": "object", + "properties": { + "year": { + "type": "string", + "readOnly": true, + "example": "2024" + }, + "month": { + "type": "string", + "readOnly": true, + "example": "3" + }, + "day": { + "type": "string", + "readOnly": true, + "example": "4" + } + }, + "description": "The SAM license info", + "readOnly": true + } + }, + "description": "The IFace info", + "readOnly": true + }, + "sam": { + "required": [ + "version" + ], + "type": "object", + "properties": { + "version": { + "type": "string", + "readOnly": true, + "example": "1.28.1" + } + }, + "description": "The SAM info", + "readOnly": true + } + }, + "readOnly": true + }, + "ActuatorHealth": { + "required": [ + "status" + ], + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The application health status", + "readOnly": true, + "example": "UP", + "enum": [ + "UP", + "DOWN" + ] + } + }, + "readOnly": true + }, + "ErrorResponse": { + "required": [ + "errorCode", + "errorMessage" + ], + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "description": "Error code", + "readOnly": true, + "enum": [ + "NOT_FOUND", + "INVALID_ID", + "INVALID_IMAGE", + "INVALID_REQUEST_BODY", + "INVALID_REQUEST_PARAMETER", + "INVALID_REQUEST_METHOD", + "CONFLICT", + "UNEXPECTED_ERROR", + "ALREADY_EXISTS", + "FACE_SIZE_MEMORY_LIMIT", + "INCOMPATIBLE_TEMPLATE", + "CORRUPTED_TEMPLATE", + "UNSUPPORTED_VERSION_TEMPLATE" + ] + }, + "errorMessage": { + "type": "string", + "description": "Error detailed description. It\u0027s only informative.", + "readOnly": true + } + }, + "description": "Error response", + "readOnly": true + }, + "CreateSelfieRequest": { + "type": "object", + "properties": { + "image": { + "$ref": "#/components/schemas/Image" + }, + "selfieOrigin": { + "$ref": "#/components/schemas/LivenessSelfieOrigin" + } + }, + "writeOnly": true + }, + "Image": { + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte" + }, + "url": { + "type": "string", + "description": "Image\u0027s URL", + "example": "http://localhost/example.png" + } + }, + "description": "Image provided as Base64 encoded string or via URL. Data or URL have to be provided.", + "writeOnly": true + }, + "LivenessSelfieOrigin": { + "required": [ + "link" + ], + "type": "object", + "properties": { + "link": { + "type": "string", + "description": "The liveness selfie origin link", + "example": "/api/v1/customers/65ebc529-50da-43cb-9963-e15fbf524f8e/liveness/records/df0d69e3-9d59-4a8b-82de-510b3950dc39/selfie" + } + }, + "description": "This allows to link the image from liveness to be selfie photo. Do not upload the photo in Image class in this case.", + "writeOnly": true + }, + "CreateSelfieResponse": { + "type": "object", + "properties": { + "detection": { + "$ref": "#/components/schemas/FaceDetection" + }, + "links": { + "$ref": "#/components/schemas/Links" + }, + "errorCode": { + "type": "string", + "description": "The face detection error code", + "readOnly": true, + "enum": [ + "NO_FACE_DETECTED" + ] + }, + "warnings": { + "type": "array", + "description": "The face detection warnings", + "readOnly": true, + "items": { + "type": "string", + "description": "The face detection warnings", + "readOnly": true, + "enum": [ + "MULTIPLE_FACES_DETECTED" + ] + } + } + }, + "readOnly": true + }, + "FaceDetection": { + "required": [ + "confidence", + "faceRectangle" + ], + "type": "object", + "properties": { + "confidence": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The face detection confidence. Values near 1.0 indicates a high confidence a human face was detected.", + "format": "double", + "readOnly": true, + "example": 0.34 + }, + "faceRectangle": { + "$ref": "#/components/schemas/Roi" + } + }, + "description": "Result of the face detection containing coordinates of rectangle where the face was detected and the confidence score there is a face.", + "readOnly": true + }, + "Links": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "The resource\u0027s self link.", + "readOnly": true + } + }, + "description": "The resource\u0027s links", + "readOnly": true + }, + "Point": { + "required": [ + "x", + "y" + ], + "type": "object", + "properties": { + "x": { + "type": "integer", + "description": "The x-axis coordinate of the point, in pixels", + "format": "int32", + "readOnly": true, + "example": 10 + }, + "y": { + "type": "integer", + "description": "The y-axis coordinate of the point, in pixels", + "format": "int32", + "readOnly": true, + "example": 20 + } + }, + "description": "The point coordinates.", + "readOnly": true + }, + "Roi": { + "required": [ + "bottomLeft", + "bottomRight", + "topLeft", + "topRight" + ], + "type": "object", + "properties": { + "topLeft": { + "$ref": "#/components/schemas/Point" + }, + "topRight": { + "$ref": "#/components/schemas/Point" + }, + "bottomRight": { + "$ref": "#/components/schemas/Point" + }, + "bottomLeft": { + "$ref": "#/components/schemas/Point" + } + }, + "description": "The region of the interest. The coordinates of the rectangle.", + "readOnly": true + }, + "CreateCustomerLivenessResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "readOnly": true + }, + "CreateDocumentRequest": { + "type": "object", + "properties": { + "advice": { + "$ref": "#/components/schemas/DocumentAdvice" + }, + "sources": { + "type": "array", + "description": "The document sources. The chosen document sources indicate which part of the document should be processed. For instance, if only MRZ is requested, the document is classified only by MRZ, and only MRZ is recognized and processed.", + "items": { + "type": "string", + "description": "The document sources. The chosen document sources indicate which part of the document should be processed. For instance, if only MRZ is requested, the document is classified only by MRZ, and only MRZ is recognized and processed.", + "enum": [ + "VIZ", + "MRZ", + "BARCODE", + "DOCUMENT_PORTRAIT" + ] + } + } + }, + "writeOnly": true + }, + "DocumentAdvice": { + "type": "object", + "properties": { + "classification": { + "$ref": "#/components/schemas/DocumentClassificationAdvice" + } + }, + "description": "Advice to the OCR document classification process about expected document type in the uploaded image.", + "writeOnly": true + }, + "DocumentClassificationAdvice": { + "type": "object", + "properties": { + "countries": { + "type": "array", + "description": "The list of Alpha-3 ISO 3166 country codes", + "example": [ + "svk", + "cze" + ], + "items": { + "type": "string", + "description": "The list of Alpha-3 ISO 3166 country codes", + "example": "[\"svk\",\"cze\"]" + } + }, + "types": { + "type": "array", + "description": "The list of the identity document types", + "example": [ + "identity-card", + "passport" + ], + "items": { + "type": "string", + "description": "The list of the identity document types", + "example": "[\"identity-card\",\"passport\"]" + } + }, + "editions": { + "type": "array", + "description": "The list of the document editions", + "example": [ + "2008-2019" + ], + "items": { + "type": "string", + "description": "The list of the document editions", + "example": "[\"2008-2019\"]" + } + }, + "machineReadableTravelDocuments": { + "type": "array", + "description": "The list of the MRZ types", + "example": [ + "td1", + "td2", + "td3" + ], + "items": { + "type": "string", + "description": "The list of the MRZ types", + "example": "[\"td1\",\"td2\",\"td3\"]" + } + } + }, + "description": "Advice to the classification process defining the expected document type, issuing country, edition or type of travel document. This is optional input. If not provided, classification will try to match among all supported documents. If provided and document in image is different, an error is returned.", + "writeOnly": true + }, + "CreateDocumentResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "readOnly": true + }, + "CreateDocumentPageRequest": { + "required": [ + "image" + ], + "type": "object", + "properties": { + "image": { + "$ref": "#/components/schemas/Image" + }, + "advice": { + "$ref": "#/components/schemas/DocumentPageAdvice" + } + }, + "writeOnly": true + }, + "DocumentPageAdvice": { + "type": "object", + "properties": { + "classification": { + "$ref": "#/components/schemas/DocumentPageClassificationAdvice" + } + }, + "description": "The document page processing advice.", + "writeOnly": true + }, + "DocumentPageClassificationAdvice": { + "required": [ + "pageTypes" + ], + "type": "object", + "properties": { + "pageTypes": { + "type": "array", + "description": "The list of the page types", + "example": [ + "front", + "back" + ], + "items": { + "type": "string", + "description": "The list of the page types", + "example": "[\"front\",\"back\"]" + } + } + }, + "description": "Advice to the OCR document classification process about expected document page in the uploaded image. This is optional input. If not provided, classification will try to match both front and back pages. If provided and document page in image is different, an error is returned.", + "writeOnly": true + }, + "CreateDocumentPageResponse": { + "type": "object", + "properties": { + "documentType": { + "$ref": "#/components/schemas/DocumentType" + }, + "pageType": { + "type": "string", + "description": "Document\u0027s page type", + "readOnly": true, + "example": "front" + }, + "detection": { + "$ref": "#/components/schemas/DocumentDetection" + }, + "errorCode": { + "type": "string", + "description": "The document page processing error code", + "readOnly": true, + "enum": [ + "NO_CARD_CORNERS_DETECTED", + "PAGE_DOESNT_MATCH_DOCUMENT_TYPE_OF_PREVIOUS_PAGE" + ] + }, + "warnings": { + "type": "array", + "description": "The document page processing warnings", + "readOnly": true, + "items": { + "type": "string", + "description": "The document page processing warnings", + "readOnly": true, + "enum": [ + "DOCUMENT_TYPE_NOT_RECOGNIZED" + ] + } + }, + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "readOnly": true + }, + "DocumentCoordinates": { + "required": [ + "bottomLeftCorner", + "bottomRightCorner", + "topLeftCorner", + "topRightCorner" + ], + "type": "object", + "properties": { + "topLeftCorner": { + "$ref": "#/components/schemas/Point" + }, + "topRightCorner": { + "$ref": "#/components/schemas/Point" + }, + "bottomLeftCorner": { + "$ref": "#/components/schemas/Point" + }, + "bottomRightCorner": { + "$ref": "#/components/schemas/Point" + } + }, + "description": "The document\u0027s position in the image. The position is defined by document corner\u0027s coordinates.", + "readOnly": true + }, + "DocumentDetection": { + "required": [ + "confidence", + "coordinates" + ], + "type": "object", + "properties": { + "confidence": { + "type": "number", + "description": "The document\u0027s detection confidence.", + "format": "double", + "example": 0.8 + }, + "coordinates": { + "$ref": "#/components/schemas/DocumentCoordinates" + } + }, + "readOnly": true + }, + "DocumentType": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "The Alpha-3 ISO 3166 country code", + "readOnly": true, + "example": "svk" + }, + "edition": { + "type": "string", + "description": "The edition", + "readOnly": true, + "example": "2008-2019" + }, + "type": { + "type": "string", + "description": "The identity document type: passport, identity-card,...", + "readOnly": true, + "example": "identity-card" + }, + "machineReadableTravelDocument": { + "type": "string", + "description": "ICAO Machine Readable Travel Document (MRTD) Specification", + "readOnly": true, + "example": "TD1" + } + }, + "description": "Recognized type of document and issuing country, in case it is recognized.", + "readOnly": true + }, + "CreateFaceRequest": { + "type": "object", + "properties": { + "image": { + "$ref": "#/components/schemas/Image" + }, + "detection": { + "$ref": "#/components/schemas/FaceDetectionProperties" + }, + "faceOrigin": { + "$ref": "#/components/schemas/CustomerSelfieOrigin" + } + }, + "writeOnly": true + }, + "CustomerSelfieOrigin": { + "required": [ + "link" + ], + "type": "object", + "properties": { + "link": { + "type": "string", + "description": "The customer selfie origin link", + "example": "/api/v1/customers/65ebc529-50da-43cb-9963-e15fbf524f8e/selfie" + } + }, + "description": "This allows to link the image from customer selfie. Do not upload the photo in Image class in this case and do not set detection properties.", + "writeOnly": true + }, + "FaceDetectionProperties": { + "type": "object", + "properties": { + "mode": { + "type": "string", + "description": "The face detection mode. The `strict` detection detects face but returns error if multiple faces are detected in the image. The `free` detection detects faces in the image and returns the biggest one with warning if there are multiple faces detected. ", + "example": "FREE", + "enum": [ + "FREE", + "STRICT" + ] + }, + "faceSizeRatio": { + "$ref": "#/components/schemas/FaceSizeRatio" + } + }, + "description": "The face detection properties", + "writeOnly": true + }, + "FaceSizeRatio": { + "required": [ + "max", + "min" + ], + "type": "object", + "properties": { + "min": { + "maximum": 1.0, + "exclusiveMaximum": false, + "minimum": 0.0, + "exclusiveMinimum": false, + "type": "number", + "description": "The minimum face size ratio", + "format": "float", + "example": 0.05 + }, + "max": { + "maximum": 1.0, + "exclusiveMaximum": false, + "minimum": 0.0, + "exclusiveMinimum": false, + "type": "number", + "description": "The maximum face size ratio", + "format": "float", + "example": 0.5 + } + }, + "description": "The face size ratio configuration. The minimum have to be less then the maximum.", + "writeOnly": true + }, + "CreateFaceResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The face\u0027s ID", + "readOnly": true + }, + "detection": { + "$ref": "#/components/schemas/FaceDetection" + }, + "links": { + "$ref": "#/components/schemas/Links" + }, + "errorCode": { + "type": "string", + "description": "The face detection error code", + "readOnly": true, + "enum": [ + "MULTIPLE_FACES_DETECTED", + "NO_FACE_DETECTED" + ] + }, + "warnings": { + "type": "array", + "description": "The face detection warnings", + "readOnly": true, + "items": { + "type": "string", + "description": "The face detection warnings", + "readOnly": true, + "enum": [ + "MULTIPLE_FACES_DETECTED" + ] + } + } + }, + "readOnly": true + }, + "FaceSimilarityRequest": { + "type": "object", + "properties": { + "referenceFace": { + "type": "string", + "description": "The reference face", + "example": "/api/v1/faces/ff0d8fb4-be47-4858-b03c-6f21b479c302" + }, + "referenceFaceTemplate": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte" + } + }, + "writeOnly": true + }, + "FaceSimilarityResponse": { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The face similarity score. The higher score indicates higher similarity of matched faces.", + "format": "double", + "readOnly": true, + "example": 0.83 + } + }, + "readOnly": true + }, + "CreateCustomerResponse": { + "required": [ + "id", + "links" + ], + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The customer\u0027s ID", + "readOnly": true + }, + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "readOnly": true + }, + "CustomerStoreRequest": { + "required": [ + "onboardingStatus" + ], + "type": "object", + "properties": { + "externalId": { + "pattern": "^[a-zA-Z\\d._-]{1,64}$", + "type": "string", + "description": "External identifier of the customer (for pairing between Trust Platform and external database), limited to alphanumeric, \u0027.\u0027, \u0027-\u0027 and \u0027_\u0027 characters and maximum length of 64. If not supplied, the UUID of the customer present in the request URL is used instead.", + "writeOnly": true, + "example": "55bd71d3-12cc-4c15-accd-b6dc8702bf3b" + }, + "onboardingStatus": { + "type": "string", + "description": "Onboarding status of the customer to be stored in the Trust Platform. Use the FINISHED status only if you have collected all required data of the customer.", + "writeOnly": true, + "example": "IN_PROGRESS", + "enum": [ + "IN_PROGRESS", + "FINISHED" + ] + } + } + }, + "CreateCustomerLivenessSelfieRequest": { + "required": [ + "assertion" + ], + "type": "object", + "properties": { + "image": { + "$ref": "#/components/schemas/Image" + }, + "selfieOrigin": { + "$ref": "#/components/schemas/SelfieOrigin" + }, + "assertion": { + "type": "string", + "description": "Definition of the active liveness challenge that the customer\u0027s face has to display in the provided selfie photo. E.g. if it is expected that the face is looking to top left corner of the screen, assertion EYE_GAZE_TOP_LEFT has to be used.", + "example": "EYE_GAZE_TOP_LEFT", + "enum": [ + "EYE_GAZE_TOP_LEFT", + "EYE_GAZE_TOP_RIGHT", + "EYE_GAZE_BOTTOM_LEFT", + "EYE_GAZE_BOTTOM_RIGHT", + "SMILE", + "NEUTRAL", + "NONE" + ] + } + }, + "writeOnly": true + }, + "SelfieOrigin": { + "required": [ + "link" + ], + "type": "object", + "properties": { + "link": { + "type": "string", + "description": "The selfie origin link", + "example": "/api/v1/customers/65ebc529-50da-43cb-9963-e15fbf524f8e/selfie" + } + }, + "description": "This allows to link the image already used for customer selfie to be reused as one of the liveness photos, ideal for using the selfie photo for passive liveness. Do not upload the photo in Image class in this case.", + "writeOnly": true + }, + "CreateCustomerLivenessSelfieResponse": { + "type": "object", + "properties": { + "errorCode": { + "type": "string", + "description": "The face detection error code", + "readOnly": true, + "enum": [ + "NO_FACE_DETECTED" + ] + }, + "warnings": { + "type": "array", + "description": "The face detection warnings", + "readOnly": true, + "items": { + "type": "string", + "description": "The face detection warnings", + "readOnly": true, + "enum": [ + "MULTIPLE_FACES_DETECTED", + "LOW_QUALITY" + ] + } + } + }, + "readOnly": true + }, + "CreateCustomerLivenessRecordResponse": { + "type": "object", + "properties": { + "selfie": { + "$ref": "#/components/schemas/SelfieFromLivenessRecord" + }, + "links": { + "$ref": "#/components/schemas/LivenessRecordLinks" + }, + "errorCode": { + "type": "string", + "description": "The face detection error code", + "readOnly": true, + "enum": [ + "INVALID_DATA" + ] + } + } + }, + "LivenessRecordLinks": { + "required": [ + "selfie" + ], + "type": "object", + "properties": { + "selfie": { + "type": "string", + "description": "The resource\u0027s selfie link.", + "readOnly": true + } + }, + "description": "The resource\u0027s links", + "readOnly": true + }, + "SelfieFromLivenessRecord": { + "required": [ + "detection" + ], + "type": "object", + "properties": { + "detection": { + "$ref": "#/components/schemas/FaceDetection" + } + }, + "readOnly": true + }, + "EvaluateCustomerLivenessRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "EYE_GAZE_LIVENESS", + "PASSIVE_LIVENESS", + "SMILE_LIVENESS", + "MAGNIFEYE_LIVENESS" + ] + } + }, + "description": "Definition of the type of liveness evaluation to be performed on the provided photos. Please, read the chapters \"Passive Liveness\" and \"Active Liveness\" of the DOT documentation.", + "writeOnly": true + }, + "EvaluateCustomerLivenessResponse": { + "type": "object", + "properties": { + "score": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The liveness score", + "format": "double", + "readOnly": true, + "example": 0.8 + }, + "errorCode": { + "type": "string", + "description": "The liveness error code", + "readOnly": true, + "example": "INVALID_DATA", + "enum": [ + "NOT_ENOUGH_DATA", + "INVALID_DATA" + ] + } + }, + "description": "Result of the liveness evaluation. Please, read the chapters \"Passive Liveness\" and \"Active Liveness\" of the DOT documentation to evaluate the calculated score.", + "readOnly": true + }, + "CustomerInspectResponse": { + "type": "object", + "properties": { + "selfieInspection": { + "$ref": "#/components/schemas/SelfieInspection" + }, + "security": { + "$ref": "#/components/schemas/SecurityInspection" + }, + "links": { + "$ref": "#/components/schemas/CustomerInspectionLinks" + } + }, + "readOnly": true + }, + "CustomerInspectionLinks": { + "required": [ + "documentInspection" + ], + "type": "object", + "properties": { + "documentInspection": { + "type": "string", + "description": "The document\u0027s inspection link", + "readOnly": true + } + }, + "description": "The customer\u0027s inspection links.", + "readOnly": true + }, + "SecurityInspection": { + "required": [ + "videoInjection" + ], + "type": "object", + "properties": { + "videoInjection": { + "$ref": "#/components/schemas/VideoInjectionInspection" + } + }, + "description": "The customer\u0027s security inspection.", + "readOnly": true + }, + "SelfieAgeDifferenceWith": { + "type": "object", + "properties": { + "documentPortrait": { + "type": "integer", + "description": "The difference in years between the estimated age of a person in the selfie and the estimated age of a person in the document portrait. The age estimated based on the document portrait is adjusted by the age of the document. This attribute is available only if the document\u0027s issue date is available.", + "format": "int32", + "readOnly": true + }, + "dateOfBirth": { + "type": "integer", + "description": "Max difference in years between the estimated age of a person in the selfie and the age extracted from the document\u0027s visual zone and MRZ.", + "format": "int32", + "readOnly": true + } + }, + "description": "Difference of the estimated age in years between the customer\u0027s selfie and other data extracted from ID document.", + "readOnly": true + }, + "SelfieInspection": { + "type": "object", + "properties": { + "similarityWith": { + "$ref": "#/components/schemas/SelfieSimilarityWith" + }, + "genderEstimate": { + "type": "string", + "description": "Estimated gender of the customer in the selfie, represented by: M for male, F for female", + "readOnly": true, + "example": "F" + }, + "genderConsistency": { + "$ref": "#/components/schemas/SelfieInspectionGenderConsistency" + }, + "ageEstimate": { + "type": "integer", + "description": "Estimated age of the customer in the selfie in years.", + "format": "int32", + "readOnly": true, + "example": 32 + }, + "ageDifferenceWith": { + "$ref": "#/components/schemas/SelfieAgeDifferenceWith" + }, + "hasMask": { + "type": "boolean", + "description": "True if a person in the selfie is wearing a face mask", + "readOnly": true, + "example": false + } + }, + "description": "Results of the inspection of customer\u0027s selfie versus document. Please, read the chapter \"Trust Factors\" of the DOT documentation.", + "readOnly": true + }, + "SelfieInspectionGenderConsistency": { + "type": "object", + "properties": { + "documentPortrait": { + "type": "boolean", + "description": "True if selfie\u0027s gender is consistent with gender from the document\u0027s portrait", + "readOnly": true + }, + "viz": { + "type": "boolean", + "description": "True if selfie\u0027s gender is consistent with gender from the document\u0027s visual zone", + "readOnly": true + }, + "mrz": { + "type": "boolean", + "description": "True if selfie\u0027s gender is consistent with gender from the document\u0027s MRZ", + "readOnly": true + } + }, + "description": "Consistency of estimated gender on the selfie with customer\u0027s data extracted from ID document.", + "readOnly": true + }, + "SelfieSimilarityWith": { + "type": "object", + "properties": { + "documentPortrait": { + "type": "boolean", + "description": "True if the person in the selfie matches a person in the document portrait", + "readOnly": true, + "example": true + }, + "livenessSelfies": { + "type": "boolean", + "description": "True if the person in the selfie matches a person in each liveness selfie", + "readOnly": true, + "example": true + } + }, + "description": "Similarity of the face between the selfie, document portrait and the liveness selfies. Please, read the chapter \"Face Matching\" of the DOT documentation.", + "readOnly": true + }, + "VideoInjectionInspection": { + "required": [ + "evaluated" + ], + "type": "object", + "properties": { + "evaluated": { + "type": "boolean", + "description": "True if video injection was evaluated on the customer\u0027s resources.", + "readOnly": true + }, + "detected": { + "type": "boolean", + "description": "True if video injection was detected on the customer\u0027s resources.", + "readOnly": true + } + }, + "description": "The video injection inspection.", + "readOnly": true + }, + "CustomerInspectDiscloseResponse": { + "type": "object", + "properties": { + "selfieInspection": { + "$ref": "#/components/schemas/SelfieInspectionDisclose" + } + }, + "readOnly": true + }, + "DocumentPortraitDisclose": { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "type": "number", + "description": "The resulting score of the similarity between the user\u0027s selfie and the document portrait.", + "format": "float", + "readOnly": true, + "example": 0.955 + } + }, + "description": "Result of the document portrait similarity check.", + "readOnly": true + }, + "SelfieInspectionDisclose": { + "type": "object", + "properties": { + "similarityWith": { + "$ref": "#/components/schemas/SelfieSimilarityWithDisclose" + } + }, + "readOnly": true + }, + "SelfieSimilarityWithDisclose": { + "type": "object", + "properties": { + "documentPortrait": { + "$ref": "#/components/schemas/DocumentPortraitDisclose" + } + }, + "description": "Similarity of the face between the selfie and the document portrait.", + "readOnly": true + }, + "AgeDifferenceWith": { + "required": [ + "dateOfBirth" + ], + "type": "object", + "properties": { + "dateOfBirth": { + "type": "integer", + "description": "Max difference with MRZ and VIZ birth date", + "format": "int32", + "readOnly": true, + "example": 7 + } + }, + "description": "Difference of age with particular fields. Only available if a difference has been detected", + "readOnly": true + }, + "BarcodesConsistency": { + "required": [ + "inconsistentTexts" + ], + "type": "object", + "properties": { + "inconsistentTexts": { + "type": "array", + "description": "All recognized text fields of the visual zone those are inconsistent with barcodes. It works only for unencrypted barcodes that represent text in the same format as in the visual zone.", + "readOnly": true, + "items": { + "type": "string", + "description": "All recognized text fields of the visual zone those are inconsistent with barcodes. It works only for unencrypted barcodes that represent text in the same format as in the visual zone.", + "readOnly": true + } + } + }, + "description": "The visual zone text consistency with document barcodes", + "readOnly": true + }, + "DocumentInspectResponse": { + "type": "object", + "properties": { + "expired": { + "type": "boolean", + "description": "True if the document is expired at the time of the customer\u0027s on-boarding. This attribute is available only if the document\u0027s expiry date is available.", + "readOnly": true, + "example": false + }, + "mrzInspection": { + "$ref": "#/components/schemas/MrzInspection" + }, + "portraitInspection": { + "$ref": "#/components/schemas/PortraitInspection" + }, + "visualZoneInspection": { + "$ref": "#/components/schemas/VisualZoneInspection" + }, + "pageTampering": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PageTamperingInspection" + }, + "readOnly": true + } + }, + "description": "Results of the inspection of customer\u0027s document. Please, read the chapter \"ID Document Authenticity Evaluation\" of the DOT documentation.", + "readOnly": true + }, + "MrzConsistency": { + "required": [ + "inconsistentTexts" + ], + "type": "object", + "properties": { + "inconsistentTexts": { + "type": "array", + "description": "All recognized text fields of visual zone that are inconsistent with MRZ.", + "readOnly": true, + "items": { + "type": "string", + "description": "All recognized text fields of visual zone that are inconsistent with MRZ.", + "readOnly": true + } + } + }, + "description": "The visual zone text consistency with document MRZ", + "readOnly": true + }, + "MrzInspection": { + "required": [ + "valid" + ], + "type": "object", + "properties": { + "valid": { + "type": "boolean", + "description": "True if the document\u0027s MRZ is valid. A valid MRZ has to match the specification and its checksums have to be correct.", + "readOnly": true, + "example": true + } + }, + "description": "Result of inspection of MRZ zone on the document, if present. Please, read the chapter \"Machine Readable Zone\" of the DOT documentation.", + "readOnly": true + }, + "OcrConfidence": { + "required": [ + "confidence" + ], + "type": "object", + "properties": { + "confidence": { + "type": "number", + "description": "The median of all recognized text fields OCR confidences.", + "format": "double", + "readOnly": true, + "example": 0.98 + }, + "lowOcrConfidenceTexts": { + "type": "array", + "description": "All recognized text fields that have OCR confidence below the configurable threshold.", + "readOnly": true, + "items": { + "type": "string", + "description": "All recognized text fields that have OCR confidence below the configurable threshold.", + "readOnly": true + } + } + }, + "description": "Confidence of the OCR recognition accuracy of the text fields in the visual inspection zone of the document.", + "readOnly": true + }, + "PageTamperingInspection": { + "type": "object", + "properties": { + "colorProfileChangeDetected": { + "type": "boolean", + "description": "True if there is a significant change in color profile of the submitted document against a supported document (e.g. the supplied photo is in greyscale)", + "readOnly": true, + "example": false + }, + "looksLikeScreenshot": { + "type": "boolean", + "description": "True if the submitted document was detected to be photographed from a screen", + "readOnly": true, + "example": false + }, + "tamperedTexts": { + "type": "boolean", + "description": "True if the submitted document shows signs of text manipulation", + "readOnly": true, + "example": false + } + }, + "description": "Result of the visual detection indicating possibly fraudulent documents based on the appearance of the image.", + "readOnly": true + }, + "PortraitInspection": { + "type": "object", + "properties": { + "genderEstimate": { + "type": "string", + "description": "Gender estimate from the document portrait", + "readOnly": true, + "example": "M" + }, + "genderConsistency": { + "$ref": "#/components/schemas/PortraitInspectionGenderConsistency" + }, + "ageEstimate": { + "type": "integer", + "description": "Estimation of age from the document portrait", + "format": "int32", + "readOnly": true, + "example": 42 + }, + "ageDifferenceWith": { + "$ref": "#/components/schemas/AgeDifferenceWith" + } + }, + "description": "Result of document portrait inspection, which checks estimated age and gender against other data on the document. Only available if the document portrait is available", + "readOnly": true + }, + "PortraitInspectionGenderConsistency": { + "type": "object", + "properties": { + "viz": { + "type": "boolean", + "description": "True if the gender is consistent with the visual zone. Only available if the visual zone of the document has been processed.", + "readOnly": true, + "example": true + }, + "mrz": { + "type": "boolean", + "description": "True if the gender is consistent with the MRZ. Only available if the document\u0027s MRZ is available", + "readOnly": true, + "example": true + } + }, + "description": "Gender consistency between document portrait and document data. Only available if MRZ and VIZ are available for comparison", + "readOnly": true + }, + "TextConsistency": { + "required": [ + "consistent" + ], + "type": "object", + "properties": { + "consistent": { + "type": "boolean", + "description": "True if the document\u0027s VIZ is consistent with other document data.", + "readOnly": true, + "example": true + }, + "consistencyWith": { + "$ref": "#/components/schemas/TextConsistentWith" + } + }, + "description": "Cross-check of the text fields extracted with OCR recognition from the visual inspection zone against the texts extracted from other sources in the document.", + "readOnly": true + }, + "TextConsistentWith": { + "type": "object", + "properties": { + "mrz": { + "$ref": "#/components/schemas/MrzConsistency" + }, + "barcodes": { + "$ref": "#/components/schemas/BarcodesConsistency" + } + }, + "description": "The visual zone text consistency with other document data", + "readOnly": true + }, + "VisualZoneInspection": { + "required": [ + "ocrConfidence" + ], + "type": "object", + "properties": { + "ocrConfidence": { + "$ref": "#/components/schemas/OcrConfidence" + }, + "textConsistency": { + "$ref": "#/components/schemas/TextConsistency" + } + }, + "description": "The document visual zone inspection result", + "readOnly": true + }, + "ColorProfileChangeDetectedDisclose": { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "type": "number", + "description": "The resulting score of the color profile check representing difference between the submitted document and the reference documents (e.g. the supplied photo is in greyscale). The score is normalized to the interval from 0 to 1. Lower the score, higher the color difference between documents.", + "format": "float", + "readOnly": true, + "example": 0.2345 + } + }, + "description": "Result of the color profile difference check.", + "readOnly": true + }, + "DocumentInspectDiscloseResponse": { + "type": "object", + "properties": { + "pageTampering": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PageTamperingInspectionDisclose" + }, + "readOnly": true + } + }, + "readOnly": true + }, + "DocumentPortraitGenuineDisclose": { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "type": "number", + "description": "The resulting score of the document portrait genuine check on the submitted document. The score is normalized to the interval from 0 to 1. Higher the score, higher the probability of the document portrait being genuine.", + "format": "float", + "readOnly": true, + "example": 0.2165 + } + }, + "description": "Result of the document portrait genuine check.", + "readOnly": true + }, + "LooksLikeScreenshotDisclose": { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "type": "number", + "description": "The resulting score of the screenshot detection on the submitted document. The score is normalized to the interval from 0 to 1. Lower the score, higher the probability of the document being photographed from the screen.", + "format": "double", + "readOnly": true, + "example": 0.1455 + } + }, + "description": "Result of the screenshot detection check.", + "readOnly": true + }, + "PageTamperingInspectionDisclose": { + "type": "object", + "properties": { + "colorProfileChangeDetected": { + "$ref": "#/components/schemas/ColorProfileChangeDetectedDisclose" + }, + "looksLikeScreenshot": { + "$ref": "#/components/schemas/LooksLikeScreenshotDisclose" + }, + "documentPortraitGenuine": { + "$ref": "#/components/schemas/DocumentPortraitGenuineDisclose" + }, + "tamperedTexts": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TamperedTextDetectionScore" + }, + "readOnly": true + } + }, + "description": "Result of the visual detection indicating possibly fraudulent documents based on the appearance of the image.", + "readOnly": true + }, + "TamperedTextDetectionScore": { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "type": "number", + "description": "The resulting score of the tampered text check for text field on the submitted document. The score is normalized to the interval from 0 to 1. Higher the score, higher the probability of the document texts being genuine.", + "format": "float", + "readOnly": true, + "example": 0.2165 + } + }, + "description": "Result of the document tampered text check.", + "readOnly": true + }, + "Document": { + "title": "MetadataDocument", + "required": [ + "documentType", + "pages" + ], + "type": "object", + "properties": { + "documentType": { + "$ref": "#/components/schemas/DocumentType" + }, + "pages": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PageMetadata" + }, + "description": "The map of document page\u0027s metadata. The map keys are page types.", + "readOnly": true + } + }, + "description": "Document metadata", + "readOnly": true + }, + "DocumentMetadataResponse": { + "required": [ + "documents" + ], + "type": "object", + "properties": { + "documents": { + "type": "array", + "description": "The list of document\u0027s metadata", + "readOnly": true, + "items": { + "$ref": "#/components/schemas/Document" + } + } + }, + "description": "Document metadata response", + "readOnly": true + }, + "PageMetadata": { + "required": [ + "visualZone" + ], + "type": "object", + "properties": { + "portrait": { + "$ref": "#/components/schemas/Portrait" + }, + "visualZone": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/TextField" + }, + "description": "The map of text field\u0027s metadata. The map keys are text field types.", + "readOnly": true + } + }, + "description": "Document page metadata", + "readOnly": true + }, + "Portrait": { + "title": "MetadataPortrait", + "required": [ + "present" + ], + "type": "object", + "properties": { + "present": { + "type": "boolean", + "description": "True if the document portrait image field is present", + "readOnly": true + } + }, + "description": "Portrait presence", + "readOnly": true + }, + "TextField": { + "title": "MetadataTextField", + "required": [ + "valueNormalized" + ], + "type": "object", + "properties": { + "label": { + "type": "string", + "description": "Label printed on card (if present)", + "readOnly": true, + "example": "Surname" + }, + "valueNormalized": { + "type": "boolean", + "description": "True if the value is being normalized", + "readOnly": true + } + }, + "description": "Text field metadata", + "readOnly": true + }, + "FaceAttribute": { + "required": [ + "preconditionsMet", + "score" + ], + "type": "object", + "properties": { + "score": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The face\u0027s attribute score", + "format": "double", + "readOnly": true, + "example": 0.24 + }, + "preconditionsMet": { + "type": "boolean", + "description": "The flag indicates if the face\u0027s attribute score is reliable.", + "readOnly": true, + "example": true + } + }, + "description": "The face\u0027s attribute", + "readOnly": true + }, + "FaceQualityResponse": { + "type": "object", + "properties": { + "sharpness": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "brightness": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "contrast": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "uniqueIntensityLevels": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "shadow": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "noseShadow": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "specularity": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "backgroundUniformity": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "redRightEye": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "redLeftEye": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "roll": { + "$ref": "#/components/schemas/HeadPoseAttribute" + }, + "yaw": { + "$ref": "#/components/schemas/HeadPoseAttribute" + }, + "pitch": { + "$ref": "#/components/schemas/HeadPoseAttribute" + }, + "eyeDistance": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "eyeGaze": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "rightEye": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "leftEye": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "mouth": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "faceSize": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "faceRelativeArea": { + "$ref": "#/components/schemas/FaceAttribute" + }, + "faceRelativeAreaInImage": { + "$ref": "#/components/schemas/FaceAttribute" + } + }, + "readOnly": true + }, + "HeadPoseAttribute": { + "required": [ + "angle", + "preconditionsMet" + ], + "type": "object", + "properties": { + "angle": { + "type": "integer", + "description": "The head pose angle", + "format": "int32", + "readOnly": true, + "example": 25 + }, + "preconditionsMet": { + "type": "boolean", + "description": "The flag indicates if the face\u0027s attribute score is reliable.", + "readOnly": true, + "example": true + } + }, + "readOnly": true + }, + "GlassesResponse": { + "type": "object", + "properties": { + "score": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The glasses score", + "format": "double", + "readOnly": true, + "example": 0.83 + }, + "tinted": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The tinted glasses score", + "format": "double", + "readOnly": true, + "example": 0.83 + }, + "heavyFrame": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The glasses with heavy frame score", + "format": "double", + "readOnly": true, + "example": 0.83 + } + }, + "readOnly": true + }, + "FaceTemplateResponse": { + "required": [ + "data", + "version" + ], + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte", + "readOnly": true + }, + "version": { + "type": "string", + "description": "The template version", + "readOnly": true, + "example": "1.13" + } + }, + "readOnly": true + }, + "FaceMaskResponse": { + "required": [ + "score" + ], + "type": "object", + "properties": { + "score": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "The face mask score", + "format": "double", + "readOnly": true, + "example": 0.83 + } + }, + "description": "The face mask", + "readOnly": true + }, + "ImageCrop": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte", + "readOnly": true + } + }, + "description": "The image crop.", + "readOnly": true + }, + "CropCoordinatesResponse": { + "required": [ + "coordinates", + "fullyCaptured" + ], + "type": "object", + "properties": { + "fullyCaptured": { + "type": "boolean", + "description": "The flag if whole face is captured in the input image", + "readOnly": true, + "example": true + }, + "coordinates": { + "$ref": "#/components/schemas/Roi" + } + }, + "readOnly": true + }, + "FaceAspectsResponse": { + "required": [ + "age", + "gender" + ], + "type": "object", + "properties": { + "age": { + "type": "integer", + "description": "The age of the face", + "format": "int32", + "readOnly": true, + "example": 24 + }, + "gender": { + "type": "number", + "description": "The gender score of the face", + "format": "double", + "readOnly": true, + "example": 0.5 + } + }, + "readOnly": true + }, + "Barcode": { + "required": [ + "data", + "type" + ], + "type": "object", + "properties": { + "data": { + "type": "string", + "description": "The data bytes as Base64 encoded string.", + "format": "byte", + "readOnly": true + }, + "type": { + "type": "string", + "description": "Barcode type", + "readOnly": true, + "example": "code_39" + } + }, + "description": "Parsed barcode", + "readOnly": true + }, + "BiometricMultiValueAttribute": { + "type": "object", + "properties": { + "visualZone": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s visual zone.", + "readOnly": true + }, + "visualZoneDuplicates": { + "type": "array", + "description": "The attribute is a duplicated field type on the document, the value is obtained from the document\u0027s visual zone.", + "readOnly": true, + "items": { + "type": "string", + "description": "The attribute is a duplicated field type on the document, the value is obtained from the document\u0027s visual zone.", + "readOnly": true + } + }, + "mrz": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s machine readable zone.", + "readOnly": true + }, + "selfie": { + "type": "string", + "description": "The attribute value obtained from the selfie.", + "readOnly": true + }, + "documentPortrait": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s portrait.", + "readOnly": true + } + }, + "description": "Values for the given biometric attribute retrieved from different sources by facial biometry or by OCR.", + "readOnly": true + }, + "Customer": { + "type": "object", + "properties": { + "age": { + "$ref": "#/components/schemas/BiometricMultiValueAttribute" + }, + "gender": { + "$ref": "#/components/schemas/BiometricMultiValueAttribute" + }, + "givenNames": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "surname": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "fullName": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "dateOfBirth": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "personalNumber": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "citizenship": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "nationality": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "document": { + "$ref": "#/components/schemas/CustomerDocument" + } + }, + "description": "Details of the customer retrieved by OCR from ID document photo and by facial biometry from the selfie. The date of birth is in format YYYY-MM-DD and the gender is represented by: M for male, F for female, X for undefined.", + "readOnly": true + }, + "CustomerDocument": { + "required": [ + "links", + "pageTypes" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/DocumentType" + }, + "pageTypes": { + "type": "array", + "description": "Document\u0027s page types", + "readOnly": true, + "items": { + "type": "string", + "description": "Document\u0027s page types", + "readOnly": true + } + }, + "dateOfIssue": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "dateOfExpiry": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "documentNumber": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "issuingAuthority": { + "$ref": "#/components/schemas/MultiValueAttribute" + }, + "additionalTexts": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/MultiValueAttributeWithoutMrz" + }, + "description": "The map of additional document texts. The map keys are text field types.", + "readOnly": true + }, + "mrz": { + "$ref": "#/components/schemas/Mrz" + }, + "barcodes": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/Barcode" + }, + "readOnly": true + }, + "readOnly": true + }, + "links": { + "$ref": "#/components/schemas/CustomerDocumentLinks" + } + }, + "description": "Details of the customer\u0027s document retrieved by OCR from ID document photo. The dates are in format YYYY-MM-DD.", + "readOnly": true + }, + "CustomerDocumentLinks": { + "type": "object", + "properties": { + "portrait": { + "type": "string", + "description": "The document\u0027s portrait link", + "readOnly": true + }, + "ghostPortrait": { + "type": "string", + "description": "The document\u0027s ghost portrait link", + "readOnly": true + }, + "signature": { + "type": "string", + "description": "The document\u0027s signature image link", + "readOnly": true + }, + "fingerprint": { + "type": "string", + "description": "The document\u0027s fingerprint image link", + "readOnly": true + }, + "pages": { + "type": "object", + "additionalProperties": { + "type": "string", + "description": "The map of links to the document\u0027s pages. The map keys are page types.", + "readOnly": true + }, + "description": "The map of links to the document\u0027s pages. The map keys are page types.", + "readOnly": true + } + }, + "description": "Links to cropped images from the customer\u0027s document.", + "readOnly": true + }, + "GetCustomerResponse": { + "type": "object", + "properties": { + "customer": { + "$ref": "#/components/schemas/Customer" + } + }, + "readOnly": true + }, + "Mrz": { + "type": "object", + "properties": { + "td1": { + "$ref": "#/components/schemas/Td1Mrz" + }, + "td2": { + "$ref": "#/components/schemas/Td2Mrz" + }, + "td3": { + "$ref": "#/components/schemas/Td3Mrz" + } + }, + "description": "Parsed machine readable zone", + "readOnly": true + }, + "MultiValueAttribute": { + "type": "object", + "properties": { + "visualZone": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s visual zone.", + "readOnly": true + }, + "visualZoneDuplicates": { + "type": "array", + "description": "The attribute is a duplicated field type on the document, the value is obtained from the document\u0027s visual zone.", + "readOnly": true, + "items": { + "type": "string", + "description": "The attribute is a duplicated field type on the document, the value is obtained from the document\u0027s visual zone.", + "readOnly": true + } + }, + "mrz": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s machine readable zone.", + "readOnly": true + }, + "barcode": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s barcode.", + "readOnly": true + } + }, + "description": "Values for the given textual attribute retrieved by OCR from different sources on the document photo.", + "readOnly": true + }, + "MultiValueAttributeWithoutMrz": { + "type": "object", + "properties": { + "visualZone": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s visual zone.", + "readOnly": true + }, + "visualZoneDuplicates": { + "type": "array", + "description": "The attribute is a duplicated field type on the document, the value is obtained from the document\u0027s visual zone.", + "readOnly": true, + "items": { + "type": "string", + "description": "The attribute is a duplicated field type on the document, the value is obtained from the document\u0027s visual zone.", + "readOnly": true + } + }, + "barcode": { + "type": "string", + "description": "The attribute value obtained from the document\u0027s barcode.", + "readOnly": true + } + }, + "description": "Values for the given textual attribute retrieved by OCR from different sources on the document photo.", + "readOnly": true + }, + "Td1Mrz": { + "required": [ + "checkDigitsValidity", + "dateOfBirth", + "dateOfExpiry", + "documentCode", + "documentNumber", + "gender", + "givenNames", + "issuingAuthority", + "nationality", + "surname" + ], + "type": "object", + "properties": { + "documentCode": { + "type": "string", + "description": "Document\u0027s code", + "readOnly": true, + "example": "P" + }, + "issuingAuthority": { + "type": "string", + "description": "Document\u0027s issuing authority", + "readOnly": true, + "example": "SVK" + }, + "surname": { + "type": "string", + "description": "Holder\u0027s surname", + "readOnly": true, + "example": "DOE" + }, + "givenNames": { + "type": "string", + "description": "Holder\u0027s given names", + "readOnly": true, + "example": "JOHN" + }, + "documentNumber": { + "type": "string", + "description": "Document\u0027s number", + "readOnly": true, + "example": "123456789" + }, + "nationality": { + "type": "string", + "description": "Holder\u0027s nationality", + "readOnly": true, + "example": "SVK" + }, + "dateOfBirth": { + "type": "string", + "description": "Holder\u0027s date of the birth, format: `YYMMDD`", + "readOnly": true, + "example": "841102" + }, + "gender": { + "type": "string", + "description": "Holder\u0027s gender, format: `M` for male, `F` for female, empty string for undefined gender.", + "readOnly": true, + "example": "M" + }, + "dateOfExpiry": { + "type": "string", + "description": "Document\u0027s date of the expiration, format: `YYMMDD`", + "readOnly": true, + "example": "261019" + }, + "optionalDataFirstLine": { + "type": "string", + "description": "Optional data - first line", + "readOnly": true, + "example": "2222" + }, + "optionalDataSecondLine": { + "type": "string", + "description": "Optional data - second line", + "readOnly": true, + "example": "2222" + }, + "checkDigitsValidity": { + "$ref": "#/components/schemas/Td1MrzCheckDigitsValidity" + } + }, + "description": "Parsed TD1 machine readable zone", + "readOnly": true + }, + "Td1MrzCheckDigitsValidity": { + "required": [ + "compositeCheckDigitValid", + "dateOfBirthCheckDigitValid", + "dateOfExpiryCheckDigitValid", + "documentNumberCheckDigitValid" + ], + "type": "object", + "properties": { + "documentNumberCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the document\u0027s number", + "readOnly": true, + "example": true + }, + "dateOfBirthCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the holder\u0027s date of the birth", + "readOnly": true, + "example": true + }, + "dateOfExpiryCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the holder\u0027s date of the expiration", + "readOnly": true, + "example": true + }, + "compositeCheckDigitValid": { + "type": "boolean", + "description": "Composite check digit\u0027s validity", + "readOnly": true, + "example": true + } + }, + "description": "Validity of MRZ check digits", + "readOnly": true + }, + "Td2Mrz": { + "required": [ + "checkDigitsValidity", + "dateOfBirth", + "dateOfExpiry", + "documentCode", + "documentNumber", + "gender", + "givenNames", + "issuingAuthority", + "nationality", + "surname" + ], + "type": "object", + "properties": { + "documentCode": { + "type": "string", + "description": "Document\u0027s code", + "readOnly": true, + "example": "P" + }, + "issuingAuthority": { + "type": "string", + "description": "Document\u0027s issuing authority", + "readOnly": true, + "example": "SVK" + }, + "surname": { + "type": "string", + "description": "Holder\u0027s surname", + "readOnly": true, + "example": "DOE" + }, + "givenNames": { + "type": "string", + "description": "Holder\u0027s given names", + "readOnly": true, + "example": "JOHN" + }, + "documentNumber": { + "type": "string", + "description": "Document\u0027s number", + "readOnly": true, + "example": "123456789" + }, + "nationality": { + "type": "string", + "description": "Holder\u0027s nationality", + "readOnly": true, + "example": "SVK" + }, + "dateOfBirth": { + "type": "string", + "description": "Holder\u0027s date of the birth, format: `YYMMDD`", + "readOnly": true, + "example": "841102" + }, + "gender": { + "type": "string", + "description": "Holder\u0027s gender, format: `M` for male, `F` for female, empty string for undefined gender.", + "readOnly": true, + "example": "M" + }, + "dateOfExpiry": { + "type": "string", + "description": "Document\u0027s date of the expiration, format: `YYMMDD`", + "readOnly": true, + "example": "261019" + }, + "optionalDataSecondLine": { + "type": "string", + "description": "Optional data - second line", + "readOnly": true, + "example": "2222" + }, + "checkDigitsValidity": { + "$ref": "#/components/schemas/Td2MrzCheckDigitsValidity" + } + }, + "description": "Parsed TD2 machine readable zone", + "readOnly": true + }, + "Td2MrzCheckDigitsValidity": { + "required": [ + "compositeCheckDigitValid", + "dateOfBirthCheckDigitValid", + "dateOfExpiryCheckDigitValid", + "documentNumberCheckDigitValid" + ], + "type": "object", + "properties": { + "documentNumberCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the document\u0027s number", + "readOnly": true, + "example": true + }, + "dateOfBirthCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the holder\u0027s date of the birth", + "readOnly": true, + "example": true + }, + "dateOfExpiryCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the holder\u0027s date of the expiration", + "readOnly": true, + "example": true + }, + "compositeCheckDigitValid": { + "type": "boolean", + "description": "Composite check digit\u0027s validity", + "readOnly": true, + "example": true + } + }, + "description": "Validity of MRZ check digits", + "readOnly": true + }, + "Td3Mrz": { + "required": [ + "checkDigitsValidity", + "dateOfBirth", + "dateOfExpiry", + "documentCode", + "documentNumber", + "gender", + "givenNames", + "issuingAuthority", + "nationality", + "surname" + ], + "type": "object", + "properties": { + "documentCode": { + "type": "string", + "description": "Document\u0027s code", + "readOnly": true, + "example": "P" + }, + "issuingAuthority": { + "type": "string", + "description": "Document\u0027s issuing authority", + "readOnly": true, + "example": "SVK" + }, + "surname": { + "type": "string", + "description": "Holder\u0027s surname", + "readOnly": true, + "example": "DOE" + }, + "givenNames": { + "type": "string", + "description": "Holder\u0027s given names", + "readOnly": true, + "example": "JOHN" + }, + "documentNumber": { + "type": "string", + "description": "Document\u0027s number", + "readOnly": true, + "example": "123456789" + }, + "nationality": { + "type": "string", + "description": "Holder\u0027s nationality", + "readOnly": true, + "example": "SVK" + }, + "dateOfBirth": { + "type": "string", + "description": "Holder\u0027s date of the birth, format: `YYMMDD`", + "readOnly": true, + "example": "841102" + }, + "gender": { + "type": "string", + "description": "Holder\u0027s gender, format: `M` for male, `F` for female, empty string for undefined gender.", + "readOnly": true, + "example": "M" + }, + "dateOfExpiry": { + "type": "string", + "description": "Document\u0027s date of the expiration, format: `YYMMDD`", + "readOnly": true, + "example": "261019" + }, + "personalNumberOrOptionalDataSecondLine": { + "type": "string", + "description": "Holder\u0027s personal number or other optional data", + "readOnly": true, + "example": "2222" + }, + "checkDigitsValidity": { + "$ref": "#/components/schemas/Td3MrzCheckDigitsValidity" + } + }, + "description": "Parsed TD3 machine readable zone", + "readOnly": true + }, + "Td3MrzCheckDigitsValidity": { + "required": [ + "compositeCheckDigitValid", + "dateOfBirthCheckDigitValid", + "dateOfExpiryCheckDigitValid", + "documentNumberCheckDigitValid", + "personalNumberCheckDigitValid" + ], + "type": "object", + "properties": { + "documentNumberCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the document\u0027s number", + "readOnly": true, + "example": true + }, + "dateOfBirthCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the holder\u0027s date of the birth", + "readOnly": true, + "example": true + }, + "dateOfExpiryCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the holder\u0027s date of the expiration", + "readOnly": true, + "example": true + }, + "personalNumberCheckDigitValid": { + "type": "boolean", + "description": "Check digit\u0027s validity of the holder\u0027s personal number", + "readOnly": true, + "example": true + }, + "compositeCheckDigitValid": { + "type": "boolean", + "description": "Composite check digit\u0027s validity", + "readOnly": true, + "example": true + } + }, + "description": "Validity of MRZ check digits", + "readOnly": true + }, + "DocumentPageQuality": { + "required": [ + "details", + "fine" + ], + "type": "object", + "properties": { + "fine": { + "type": "boolean", + "description": "The quality check result", + "readOnly": true, + "example": false + }, + "issues": { + "type": "array", + "description": "The list of the quality check issues if the quality check failed", + "example": "BRIGHTNESS_HIGH", + "items": { + "type": "string", + "description": "The list of the quality check issues if the quality check failed", + "example": "BRIGHTNESS_HIGH", + "enum": [ + "BRIGHTNESS_HIGH", + "BRIGHTNESS_LOW", + "HOTSPOTS_SCORE_HIGH", + "SHARPNESS_LOW", + "DOCUMENT_SMALL", + "DOCUMENT_OUT_OF_IMAGE" + ] + } + }, + "warnings": { + "type": "array", + "description": "The list of warnings from the quality check", + "readOnly": true, + "example": "DOCUMENT_CLOSE_TO_IMAGE_BORDER", + "items": { + "type": "string", + "description": "The list of warnings from the quality check", + "readOnly": true, + "example": "DOCUMENT_CLOSE_TO_IMAGE_BORDER", + "enum": [ + "DOCUMENT_CLOSE_TO_IMAGE_BORDER" + ] + } + }, + "details": { + "$ref": "#/components/schemas/QualityDetails" + } + }, + "readOnly": true + }, + "QualityCheckDetail": { + "required": [ + "level", + "score" + ], + "type": "object", + "properties": { + "score": { + "maximum": 1, + "minimum": 0, + "type": "number", + "description": "Quality parameter score", + "format": "double", + "readOnly": true, + "example": 0.45 + }, + "level": { + "type": "string", + "description": "Quality parameter level", + "readOnly": true, + "example": "LOW", + "enum": [ + "LOW", + "MEDIUM", + "HIGH" + ] + } + }, + "description": "Quality check detail", + "readOnly": true + }, + "QualityDetails": { + "required": [ + "brightness", + "hotspots", + "sharpness" + ], + "type": "object", + "properties": { + "sharpness": { + "$ref": "#/components/schemas/QualityCheckDetail" + }, + "brightness": { + "$ref": "#/components/schemas/QualityCheckDetail" + }, + "hotspots": { + "$ref": "#/components/schemas/QualityCheckDetail" + } + }, + "description": "Quality check details", + "readOnly": true + } + }, + "securitySchemes": { + "api": { + "type": "http", + "in": "header", + "scheme": "bearer" + } + } + } +} \ No newline at end of file diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/EnrollmentServerTestApplication.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/EnrollmentServerTestApplication.java new file mode 100644 index 000000000..107addd41 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/EnrollmentServerTestApplication.java @@ -0,0 +1,28 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Jan Pesek, jan.pesek@wultra.com + */ +@SpringBootApplication +class EnrollmentServerTestApplication { + +} diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index 040c7ebfd..10ba0fb81 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -46,6 +46,11 @@ enrollment-server-onboarding-api + + com.wultra.security + enrollment-server-onboarding-provider-innovatrics + + com.wultra.security enrollment-server-onboarding-provider-iproov diff --git a/pom.xml b/pom.xml index 3eec7fb00..39056d4bf 100644 --- a/pom.xml +++ b/pom.xml @@ -82,12 +82,13 @@ enrollment-server-onboarding-adapter-mock enrollment-server-onboarding-common enrollment-server-onboarding-domain-model + enrollment-server-onboarding-provider-innovatrics enrollment-server-onboarding-provider-iproov enrollment-server-onboarding-provider-zenid - 6.5.0 + 7.0.0 5.10.0 3.2.1 @@ -127,6 +128,12 @@ ${project.version} + + com.wultra.security + enrollment-server-onboarding-provider-innovatrics + ${project.version} + + com.wultra.security enrollment-server-onboarding-provider-iproov From d31c024a6b5a05917c90e375d11a78ee2bb9ff47 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Fri, 24 Nov 2023 07:42:44 +0100 Subject: [PATCH 22/52] Fix #926: Wrong iProov configuration properties in documentation --- docs/onboarding/Configuration-Properties.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index 25fc6f664..721d37125 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -127,12 +127,12 @@ The Onboarding Server uses the following public configuration properties: | `enrollment-server-onboarding.presence-check.iproov.oAuthClientUsername` | | OAuth client username to iProov REST service. | | `enrollment-server-onboarding.presence-check.iproov.oAuthClientPassword` | | OAuth client password to iProov REST service. | | `enrollment-server-onboarding.presence-check.iproov.restClientConfig.acceptInvalidSslCertificate` | `false` | Whether invalid SSL certificate is accepted when calling Zen ID REST service. | -| `enrollment-server-onboarding.document-verification.zenid.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling iProov REST service. | -| `enrollment-server-onboarding.presence-check.iproov.zenid.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling iProov REST service. | -| `enrollment-server-onboarding.presence-check.iproov.zenid.restClientConfig.proxyHost` | | Proxy host to be used when calling iProov REST service. | -| `enrollment-server-onboarding.presence-check.iproov.zenid.restClientConfig.proxyPort` | 0 | Proxy port to be used when calling iProov REST service. | -| `enrollment-server-onboarding.presence-check.iproov.zenid.restClientConfig.proxyUsername` | | Proxy username to be used when calling iProov REST service. | -| `enrollment-server-onboarding.presence-check.iproov.zenid.restClientConfig.proxyPassword` | | Proxy password to be used when calling iProov REST service. | +| `enrollment-server-onboarding.presence-check.iproov.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling iProov REST service. | +| `enrollment-server-onboarding.presence-check.iproov.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling iProov REST service. | +| `enrollment-server-onboarding.presence-check.iproov.restClientConfig.proxyHost` | | Proxy host to be used when calling iProov REST service. | +| `enrollment-server-onboarding.presence-check.iproov.restClientConfig.proxyPort` | 0 | Proxy port to be used when calling iProov REST service. | +| `enrollment-server-onboarding.presence-check.iproov.restClientConfig.proxyUsername` | | Proxy username to be used when calling iProov REST service. | +| `enrollment-server-onboarding.presence-check.iproov.restClientConfig.proxyPassword` | | Proxy password to be used when calling iProov REST service. | ## Correlation HTTP Header Configuration From 0d62bf689256b1f9f8fc7a3672b7918601a6e475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Fri, 24 Nov 2023 16:17:25 +0100 Subject: [PATCH 23/52] Fix #928: Innovatrics Configuration (#929) * Fix #928: Innovatrics Configuration --- docs/onboarding/Configuration-Properties.md | 17 +++++ .../innovatrics/InnovatricsApiService.java | 70 +++++++++++++++++++ .../innovatrics/InnovatricsConfig.java | 68 ++++++++++++++++++ .../innovatrics/InnovatricsConfigProps.java | 63 +++++++++++++++++ .../provider/iproov/IProovConfig.java | 9 ++- .../provider/zenid/ZenidConfig.java | 7 +- .../src/main/resources/application.properties | 19 +++++ 7 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfig.java create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index 721d37125..330412aaa 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -134,6 +134,23 @@ The Onboarding Server uses the following public configuration properties: | `enrollment-server-onboarding.presence-check.iproov.restClientConfig.proxyUsername` | | Proxy username to be used when calling iProov REST service. | | `enrollment-server-onboarding.presence-check.iproov.restClientConfig.proxyPassword` | | Proxy password to be used when calling iProov REST service. | + +## Innovatrics Configuration + +| Property | Default | Note | +|--------------------------------------------------------------------------------------------------|---------------------------|--------------------------------------------------------------------------------| +| `enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl` | | Base REST service URL for Innovatrics. | +| `enrollment-server-onboarding.provider.innovatrics.serviceToken` | | Authentication token for Innovatrics. | +| `enrollment-server-onboarding.provider.innovatrics.serviceUserAgent` | `Wultra/OnboardingServer` | User agent to use when making HTTP calls to Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate` | `false` | Whether invalid SSL certificate is accepted when calling Zen ID REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyHost` | | Proxy host to be used when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPort` | 0 | Proxy port to be used when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyUsername` | | Proxy username to be used when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPassword` | | Proxy password to be used when calling Innovatrics REST service. | + + ## Correlation HTTP Header Configuration | Property | Default | Note | diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java new file mode 100644 index 000000000..92260f9b8 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java @@ -0,0 +1,70 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.core.rest.client.base.RestClient; +import com.wultra.core.rest.client.base.RestClientException; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +/** + * Implementation of the REST service toInnovatrics. + *

    + * It is not possible to combine Innovatrics with other providers such as iProov or ZenID. + * Both providers, document verifier and presence check, must be configured to {@code innovatrics}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ConditionalOnExpression(""" + '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and '${enrollment-server-onboarding.document-verification.provider}' == 'innovatrics' + """) +@Service +@Slf4j +class InnovatricsApiService { + + private static final ParameterizedTypeReference STRING_TYPE_REFERENCE = new ParameterizedTypeReference<>() { }; + + /** + * REST client for Innovatrics calls. + */ + private final RestClient restClient; + + /** + * Service constructor. + * + * @param restClient REST template for Innovatrics calls. + */ + @Autowired + public InnovatricsApiService(@Qualifier("restClientInnovatrics") final RestClient restClient) { + this.restClient = restClient; + } + + // TODO remove - temporal test call + @PostConstruct + public void testCall() throws RestClientException { + logger.info("Trying a test call"); + final ResponseEntity response = restClient.get("/api/v1/metadata", STRING_TYPE_REFERENCE); + logger.info("Result of test call: {}", response.getBody()); + } +} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfig.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfig.java new file mode 100644 index 000000000..6781e1e87 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfig.java @@ -0,0 +1,68 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.core.rest.client.base.DefaultRestClient; +import com.wultra.core.rest.client.base.RestClient; +import com.wultra.core.rest.client.base.RestClientConfiguration; +import com.wultra.core.rest.client.base.RestClientException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; + +/** + * Innovatrics configuration. + *

    + * It is not possible to combine Innovatrics with other providers such as iProov or ZenID. + * Both providers, document verifier and presence check, must be configured to {@code innovatrics}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ConditionalOnExpression(""" + '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and '${enrollment-server-onboarding.document-verification.provider}' == 'innovatrics' + """) +@ComponentScan(basePackages = {"com.wultra.app.onboardingserver.provider.innovatrics"}) +@Configuration +@Slf4j +class InnovatricsConfig { + + /** + * Prepares REST client specific to Innovatrics. + * + * @param configProps Configuration properties + * @return REST client for Innovatrics service API calls. + */ + @Bean("restClientInnovatrics") + public RestClient restClientInnovatrics(final InnovatricsConfigProps configProps) throws RestClientException { + final String serviceBaseUrl = configProps.getServiceBaseUrl(); + logger.info("Registering restClientInnovatrics: {}", serviceBaseUrl); + + final HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.USER_AGENT, configProps.getServiceUserAgent()); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + configProps.getServiceToken()); + + final RestClientConfiguration restClientConfiguration = configProps.getRestClientConfig(); + restClientConfiguration.setBaseUrl(serviceBaseUrl); + restClientConfiguration.setDefaultHttpHeaders(headers); + return new DefaultRestClient(restClientConfiguration); + } + +} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java new file mode 100644 index 000000000..b12c4970c --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java @@ -0,0 +1,63 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.core.rest.client.base.RestClientConfiguration; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * Innovatrics configuration properties. + *

    + * It is not possible to combine Innovatrics with other providers such as iProov or ZenID. + * Both providers, document verifier and presence check, must be configured to {@code innovatrics}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ConditionalOnExpression(""" + '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and '${enrollment-server-onboarding.document-verification.provider}' == 'innovatrics' + """) +@Configuration +@ConfigurationProperties(prefix = "enrollment-server-onboarding.provider.innovatrics") +@Getter @Setter +class InnovatricsConfigProps { + + /** + * Service base URL. + */ + private String serviceBaseUrl; + + /** + * Authentication for Innovatrics. + */ + private String serviceToken; + + /** + * Identification of the application calling the REST services passed as the User-Agent header. + */ + private String serviceUserAgent; + + /** + * REST client configuration. + */ + private RestClientConfiguration restClientConfig; + +} diff --git a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfig.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfig.java index b1fd137cb..009867461 100644 --- a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfig.java +++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovConfig.java @@ -93,11 +93,14 @@ public ObjectMapper objectMapperIproov() { */ @Bean("restClientIProov") public RestClient restClientIProov(IProovConfigProps configProps) throws RestClientException { - HttpHeaders headers = new HttpHeaders(); + final String serviceBaseUrl = configProps.getServiceBaseUrl(); + logger.info("Registering restClientIProov: {}", serviceBaseUrl); + + final HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.USER_AGENT, configProps.getServiceUserAgent()); - RestClientConfiguration restClientConfiguration = configProps.getRestClientConfig(); - restClientConfiguration.setBaseUrl(configProps.getServiceBaseUrl()); + final RestClientConfiguration restClientConfiguration = configProps.getRestClientConfig(); + restClientConfiguration.setBaseUrl(serviceBaseUrl); restClientConfiguration.setDefaultHttpHeaders(headers); return new DefaultRestClient(restClientConfiguration); } diff --git a/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfig.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfig.java index a8a667614..53ff2da50 100644 --- a/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfig.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidConfig.java @@ -23,6 +23,7 @@ import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientConfiguration; import com.wultra.core.rest.client.base.RestClientException; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -41,6 +42,7 @@ @ConditionalOnProperty(value = "enrollment-server-onboarding.document-verification.provider", havingValue = "zenid") @ComponentScan(basePackages = {"com.wultra.app.onboardingserver.provider.zenid"}) @Configuration +@Slf4j class ZenidConfig { /** @@ -74,13 +76,16 @@ private static JavaTimeModule createJavaTimeModule() { */ @Bean("restClientZenid") public RestClient restClientZenid(final ZenidConfigProps configProps) throws RestClientException { + final String serviceBaseUrl = configProps.getServiceBaseUrl(); + logger.info("Registering restClientZenid: {}", serviceBaseUrl); + final HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); headers.add(HttpHeaders.AUTHORIZATION, "api_key " + configProps.getApiKey()); headers.add(HttpHeaders.USER_AGENT, configProps.getServiceUserAgent()); final RestClientConfiguration restClientConfiguration = configProps.getRestClientConfig(); - restClientConfiguration.setBaseUrl(configProps.getServiceBaseUrl()); + restClientConfiguration.setBaseUrl(serviceBaseUrl); restClientConfiguration.setDefaultHttpHeaders(headers); return new DefaultRestClient(restClientConfiguration, createJavaTimeModule()); } diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index ebb5fd26d..a35d7ba1c 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -97,6 +97,7 @@ enrollment-server-onboarding.identity-verification.max-failed-attempts-document- # Provider Configuration #enrollment-server-onboarding.document-verification.provider=zenid +#enrollment-server-onboarding.document-verification.provider=innovatrics enrollment-server-onboarding.document-verification.provider=mock enrollment-server-onboarding.document-verification.cleanupEnabled=false enrollment-server-onboarding.document-verification.checkInProgressDocumentSubmits.cron=- @@ -107,6 +108,7 @@ enrollment-server-onboarding.document-verification.required.count=2 enrollment-server-onboarding.presence-check.enabled=true #enrollment-server-onboarding.presence-check.provider=iproov +#enrollment-server-onboarding.presence-check.provider=innovatrics enrollment-server-onboarding.presence-check.provider=mock enrollment-server-onboarding.presence-check.cleanupEnabled=false # Enables/disabled verification of the presence check selfie photo with the documents @@ -162,6 +164,23 @@ enrollment-server-onboarding.presence-check.iproov.restClientConfig.connectionTi enrollment-server-onboarding.presence-check.iproov.restClientConfig.responseTimeout=60000 enrollment-server-onboarding.presence-check.iproov.restClientConfig.maxIdleTime=200s +# Innovatrics configuration +enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl=${INNOVATRICS_SERVICE_BASE_URL} +enrollment-server-onboarding.provider.innovatrics.serviceToken=${INNOVATRICS_SERVICE_TOKEN} +enrollment-server-onboarding.provider.innovatrics.serviceUserAgent=Wultra/OnboardingServer + +# Innovatrics REST client configuration +enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate=false +enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize=10485760 +enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyEnabled=false +enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyHost= +enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPort=0 +enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyUsername= +enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPassword= +enrollment-server-onboarding.provider.innovatrics.restClientConfig.connectionTimeout=10000 +enrollment-server-onboarding.provider.innovatrics.restClientConfig.responseTimeout=60000 +enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxIdleTime=200s + spring.security.oauth2.client.provider.app.token-uri=http://localhost:6060/oauth/token enrollment-server-onboarding.state-machine.changeMachineState.cron=0/3 * * * * * From f4e36f02a07155d9813695496e8bcc9ab2115466 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 05:52:28 +0000 Subject: [PATCH 24/52] Bump org.springframework.boot:spring-boot-starter-parent Bumps [org.springframework.boot:spring-boot-starter-parent](https://github.com/spring-projects/spring-boot) from 3.1.5 to 3.1.6. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.1.5...v3.1.6) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-parent dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 39056d4bf..22b5f3556 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.5 + 3.1.6 From 720e1def8e26ec4731d786b8cacec94e974735f2 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Mon, 27 Nov 2023 12:50:09 +0100 Subject: [PATCH 25/52] Fix #933: PresenceCheckInitResponse missing in swagger --- .../api/IdentityVerificationController.java | 2 +- .../IdentityVerificationRestService.java | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java index f5cf4700c..981c99bad 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/controller/api/IdentityVerificationController.java @@ -229,7 +229,7 @@ public ObjectResponse initVerificationSdk( @PowerAuth(resourceId = "/api/identity/presence-check/init", signatureType = { PowerAuthSignatureTypes.POSSESSION }) - public ResponseEntity initPresenceCheck(@EncryptedRequestBody ObjectRequest request, + public ResponseEntity> initPresenceCheck(@EncryptedRequestBody ObjectRequest request, @Parameter(hidden = true) EncryptionContext encryptionContext, @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, OnboardingProcessException { diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java index 961cd7886..ad18671cb 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationRestService.java @@ -24,14 +24,14 @@ import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.VerificationSdkInfo; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.OnboardingProcessEntity; import com.wultra.app.onboardingserver.common.errorhandling.*; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.configuration.OnboardingConfig; import com.wultra.app.onboardingserver.errorhandling.DocumentSubmitException; -import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.impl.service.document.DocumentProcessingService; import com.wultra.app.onboardingserver.impl.service.validation.OnboardingConsentApprovalRequestValidator; import com.wultra.app.onboardingserver.impl.service.validation.OnboardingConsentTextRequestValidator; @@ -41,7 +41,6 @@ import com.wultra.app.onboardingserver.statemachine.enums.OnboardingState; import com.wultra.app.onboardingserver.statemachine.service.StateMachineService; import io.getlime.core.rest.model.base.request.ObjectRequest; -import io.getlime.core.rest.model.base.response.ErrorResponse; import io.getlime.core.rest.model.base.response.ObjectResponse; import io.getlime.core.rest.model.base.response.Response; import io.getlime.security.powerauth.rest.api.spring.authentication.PowerAuthApiAuthentication; @@ -58,6 +57,7 @@ import org.springframework.statemachine.StateMachine; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; import java.util.Map; @@ -342,7 +342,7 @@ public ObjectResponse initVerificationSdk( * @throws OnboardingProcessException Thrown when onboarding process is invalid. */ @Transactional - public ResponseEntity initPresenceCheck(ObjectRequest request, + public ResponseEntity> initPresenceCheck(ObjectRequest request, EncryptionContext encryptionContext, PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, OnboardingProcessException { @@ -359,7 +359,10 @@ public ResponseEntity initPresenceCheck(ObjectRequest stateMachine = stateMachineService.processStateMachineEvent(ownerId, processId, OnboardingEvent.PRESENCE_CHECK_INIT); - return createResponseEntity(stateMachine); + + @SuppressWarnings("unchecked") + final Class> presenceCheckInitResponseClass = (Class>) new ObjectResponse().getClass(); + return createResponseEntity(stateMachine, presenceCheckInitResponseClass); } /** @@ -622,14 +625,14 @@ private OwnerId extractOwnerId(final String activationId) throws OnboardingProce return ownerId; } - private ResponseEntity createResponseEntity(StateMachine stateMachine) { - Response response = stateMachine.getExtendedState().get(ExtendedStateVariable.RESPONSE_OBJECT, Response.class); - HttpStatus status = stateMachine.getExtendedState().get(ExtendedStateVariable.RESPONSE_STATUS, HttpStatus.class); - if (response == null || status == null) { - logger.warn("Missing one of important values to generate response entity, response={}, status={}", response, status); - response = new ErrorResponse("UNEXPECTED_ERROR", "Unexpected error occurred."); - status = HttpStatus.INTERNAL_SERVER_ERROR; - } + private ResponseEntity createResponseEntity(final StateMachine stateMachine) { + return createResponseEntity(stateMachine, Response.class); + } + + private ResponseEntity createResponseEntity(final StateMachine stateMachine, Class responseClass) { + final T response = stateMachine.getExtendedState().get(ExtendedStateVariable.RESPONSE_OBJECT, responseClass); + final HttpStatus status = stateMachine.getExtendedState().get(ExtendedStateVariable.RESPONSE_STATUS, HttpStatus.class); + Assert.state(response != null && status != null, "Missing one of important values to generate response entity, response=%s, status=%s".formatted(response, status)); return new ResponseEntity<>(response, status); } From f77aff8b0c28dd50f9dcafb55cf097526f98a389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20=C5=A0trobl?= Date: Thu, 30 Nov 2023 18:32:50 +0100 Subject: [PATCH 26/52] Fix #935: Endpoints for operation detail and operation claim (#937) --- docs/Mobile-Token-API.md | 279 ++++++++++++++++++ .../controller/api/MobileTokenController.java | 74 +++++ .../impl/service/MobileTokenService.java | 82 ++++- .../request/OperationApproveRequest.java | 3 +- .../model/request/OperationClaimRequest.java | 37 +++ .../model/request/OperationDetailRequest.java | 34 +++ .../model/request/OperationRejectRequest.java | 4 +- .../model/request/PushRegisterRequest.java | 6 +- 8 files changed, 510 insertions(+), 9 deletions(-) create mode 100644 mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationClaimRequest.java create mode 100644 mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationDetailRequest.java diff --git a/docs/Mobile-Token-API.md b/docs/Mobile-Token-API.md index a3eaa723c..2647df518 100644 --- a/docs/Mobile-Token-API.md +++ b/docs/Mobile-Token-API.md @@ -17,6 +17,8 @@ Following endpoints are published in Enrollment Server RESTful API: ### Operations API - `POST` [/api/auth/token/app/operation/list](#get-pending-operations) - List pending Mobile Token operations +- `POST` [/api/auth/token/app/operation/detail](#get-operation-detail) - Get detail of a Mobile Token operation +- `POST` [/api/auth/token/app/operation/detail/claim](#claim-operation) - Claim a Mobile Token operation for a user - `POST` [/api/auth/token/app/operation/history](#get-history-of-operations) - Get history of Mobile Token operations - `POST` [/api/auth/token/app/operation/authorize](#confirm-operation) - Confirm a Mobile Token operation - `POST` [/api/auth/token/app/operation/cancel](#reject-operation) - Reject a Mobile Token operation @@ -226,6 +228,283 @@ Get the list with all operations that are pending confirmation. ``` + +### Get Operation Detail + +Get an operation detail. + + + + + + + + + + + + +
    MethodPOST
    Resource URI/api/auth/token/app/operation/detail
    + + +#### Request + +- Headers: + - `Content-Type: application/json` + - `Accept-Language: en-US` + - `X-PowerAuth-Authorization: ...` + +```json +{ + "requestObject": { + "id": "7e0ba60f-bf22-4ff5-b999-2733784e5eaa" + } +} +``` + +#### Response 200 + +```json +{ + "status": "OK", + "responseObject": { + "id": "7e0ba60f-bf22-4ff5-b999-2733784e5eaa", + "name": "authorize_payment", + "data": "A1*A100CZK*Q238400856/0300**D20190629*NUtility Bill Payment - 05/2019", + "status": "PENDING", + "operationCreated": "2018-07-02T14:43:13+0000", + "operationExpires": "2018-07-02T14:48:17+0000", + "allowedSignatureType": { + "type": "2FA", + "variants": [ + "possession_knowledge", + "possession_biometry" + ] + }, + "formData": { + "title": "Confirm Payment", + "message": "Hello,\nplease confirm following payment:", + "attributes": [ + { + "type": "ALERT", + "alertType": "WARNING", + "id": "operation.warning", + "label": "Balance alert", + "title": "Insufficient Balance", + "message": "You have only $1.00 on your account with number 238400856/0300." + }, + { + "type": "HEADING", + "id": "operation.heading", + "label": "Utility Payment" + }, + { + "type": "AMOUNT", + "id": "operation.amount", + "label": "Amount", + "amount": 2199.40, + "currency": "CZK", + "amountFormatted": "2199,40", + "currencyFormatted": "Kč" + }, + { + "type": "AMOUNT_CONVERSION", + "id": "operation.conversion", + "label": "Conversion Rate", + "dynamic": false, + "sourceAmount": 100.00, + "sourceCurrency": "USD", + "sourceAmountFormatted": "100.00", + "sourceCurrencyFormatted": "$", + "sourceValueFormatted": "$100.00", + "targetAmount": 2199.40, + "targetCurrency": "CZK", + "targetAmountFormatted": "2199,40", + "targetCurrencyFormatted": "Kč", + "targetValueFormatted": "2199,40 Kč" + }, + { + "type": "KEY_VALUE", + "id": "operation.account", + "label": "To Account", + "value": "238400856/0300" + }, + { + "type": "KEY_VALUE", + "id": "operation.dueDate", + "label": "Due Date", + "value": "Jun 29, 2019" + }, + { + "type": "NOTE", + "id": "operation.note", + "label": "Note", + "note": "Utility Bill Payment - 05/2019" + }, + { + "type": "IMAGE", + "id": "operation.image", + "label": "Payment Check Preview", + "thumbnailUrl": "https://example.com/thumbnail.png", + "originalUrl": "https://example.com/image.png" + }, + { + "type": "PARTY_INFO", + "id": "operation.partyInfo", + "label": "Application", + "partyInfo": { + "logoUrl": "https://itesco.cz/img/logo/logo.svg", + "name": "Tesco", + "description": "Find out more about Tesco...", + "websiteUrl": "https://itesco.cz/hello" + } + } + ] + } + } +} +``` + + + +### Claim Operation + +Claim an operation for a user. + + + + + + + + + + + + +
    MethodPOST
    Resource URI/api/auth/token/app/operation/detail/claim
    + + +#### Request + +- Headers: + - `Content-Type: application/json` + - `Accept-Language: en-US` + - `X-PowerAuth-Authorization: ...` + +```json +{ + "requestObject": { + "id": "7e0ba60f-bf22-4ff5-b999-2733784e5eaa", + "userId": "user12345" + } +} +``` + +#### Response 200 + +```json +{ + "status": "OK", + "responseObject": { + "id": "7e0ba60f-bf22-4ff5-b999-2733784e5eaa", + "name": "authorize_payment", + "data": "A1*A100CZK*Q238400856/0300**D20190629*NUtility Bill Payment - 05/2019", + "status": "PENDING", + "operationCreated": "2018-07-02T14:43:13+0000", + "operationExpires": "2018-07-02T14:48:17+0000", + "allowedSignatureType": { + "type": "2FA", + "variants": [ + "possession_knowledge", + "possession_biometry" + ] + }, + "formData": { + "title": "Confirm Payment", + "message": "Hello,\nplease confirm following payment:", + "attributes": [ + { + "type": "ALERT", + "alertType": "WARNING", + "id": "operation.warning", + "label": "Balance alert", + "title": "Insufficient Balance", + "message": "You have only $1.00 on your account with number 238400856/0300." + }, + { + "type": "HEADING", + "id": "operation.heading", + "label": "Utility Payment" + }, + { + "type": "AMOUNT", + "id": "operation.amount", + "label": "Amount", + "amount": 2199.40, + "currency": "CZK", + "amountFormatted": "2199,40", + "currencyFormatted": "Kč" + }, + { + "type": "AMOUNT_CONVERSION", + "id": "operation.conversion", + "label": "Conversion Rate", + "dynamic": false, + "sourceAmount": 100.00, + "sourceCurrency": "USD", + "sourceAmountFormatted": "100.00", + "sourceCurrencyFormatted": "$", + "sourceValueFormatted": "$100.00", + "targetAmount": 2199.40, + "targetCurrency": "CZK", + "targetAmountFormatted": "2199,40", + "targetCurrencyFormatted": "Kč", + "targetValueFormatted": "2199,40 Kč" + }, + { + "type": "KEY_VALUE", + "id": "operation.account", + "label": "To Account", + "value": "238400856/0300" + }, + { + "type": "KEY_VALUE", + "id": "operation.dueDate", + "label": "Due Date", + "value": "Jun 29, 2019" + }, + { + "type": "NOTE", + "id": "operation.note", + "label": "Note", + "note": "Utility Bill Payment - 05/2019" + }, + { + "type": "IMAGE", + "id": "operation.image", + "label": "Payment Check Preview", + "thumbnailUrl": "https://example.com/thumbnail.png", + "originalUrl": "https://example.com/image.png" + }, + { + "type": "PARTY_INFO", + "id": "operation.partyInfo", + "label": "Application", + "partyInfo": { + "logoUrl": "https://itesco.cz/img/logo/logo.svg", + "name": "Tesco", + "description": "Find out more about Tesco...", + "websiteUrl": "https://itesco.cz/hello" + } + } + ] + } + } +} +``` + + ### Get History of Operations diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java index 35358964a..1a901b16f 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java @@ -26,7 +26,9 @@ import com.wultra.core.http.common.request.RequestContext; import com.wultra.core.http.common.request.RequestContextConverter; import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation; import com.wultra.security.powerauth.lib.mtoken.model.request.OperationApproveRequest; +import com.wultra.security.powerauth.lib.mtoken.model.request.OperationDetailRequest; import com.wultra.security.powerauth.lib.mtoken.model.request.OperationRejectRequest; import com.wultra.security.powerauth.lib.mtoken.model.response.MobileTokenResponse; import com.wultra.security.powerauth.lib.mtoken.model.response.OperationListResponse; @@ -118,6 +120,78 @@ public ObjectResponse operationList(@Parameter(hidden = t } } + /** + * Get the detail of an operation. + * + * @param auth Authentication object. + * @param locale Locale. + * @return List of pending operations. + * @throws MobileTokenException In the case error mobile token service occurs. + * @throws MobileTokenConfigurationException In the case of system misconfiguration. + */ + @PostMapping("/operation/detail") + @PowerAuthToken(signatureType = { + PowerAuthSignatureTypes.POSSESSION, + PowerAuthSignatureTypes.POSSESSION_BIOMETRY, + PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE, + PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE_BIOMETRY + }) + public ObjectResponse getOperationDetail(@RequestBody ObjectRequest request, + @Parameter(hidden = true) PowerAuthApiAuthentication auth, + @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException { + try { + if (auth != null) { + final String operationId = request.getRequestObject().getId(); + final String language = locale.getLanguage(); + final String userId = auth.getUserId(); + final Operation response = mobileTokenService.getOperationDetail(operationId, language, userId); + final Date currentTimestamp = new Date(); + return new MobileTokenResponse<>(response, currentTimestamp); + } else { + throw new MobileTokenAuthException(); + } + } catch (PowerAuthClientException e) { + logger.error("Unable to call upstream service.", e); + throw new MobileTokenAuthException(); + } + } + + /** + * Claim operation for a user. + * + * @param auth Authentication object. + * @param locale Locale. + * @return List of pending operations. + * @throws MobileTokenException In the case error mobile token service occurs. + * @throws MobileTokenConfigurationException In the case of system misconfiguration. + */ + @PostMapping("/operation/detail/claim") + @PowerAuthToken(signatureType = { + PowerAuthSignatureTypes.POSSESSION, + PowerAuthSignatureTypes.POSSESSION_BIOMETRY, + PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE, + PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE_BIOMETRY + }) + public ObjectResponse claimOperation(@RequestBody ObjectRequest request, + @Parameter(hidden = true) PowerAuthApiAuthentication auth, + @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException { + try { + if (auth != null) { + final String operationId = request.getRequestObject().getId(); + final String language = locale.getLanguage(); + final String userId = auth.getUserId(); + final Operation response = mobileTokenService.claimOperation(operationId, language, userId); + final Date currentTimestamp = new Date(); + return new MobileTokenResponse<>(response, currentTimestamp); + } else { + throw new MobileTokenAuthException(); + } + } catch (PowerAuthClientException e) { + logger.error("Unable to call upstream service.", e); + throw new MobileTokenAuthException(); + } + } + /** * Get the list of all operations. * diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java index 8d70fac99..b2f87be5a 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java @@ -144,7 +144,7 @@ public OperationListResponse operationListForUser( */ public Response operationApprove(@NotNull final OperationApproveParameterObject request) throws MobileTokenException, PowerAuthClientException { - final OperationDetailResponse operationDetail = getOperationDetail(request.getOperationId()); + final OperationDetailResponse operationDetail = claimOperationInternal(request.getOperationId(), null); final String activationFlag = operationDetail.getActivationFlag(); if (activationFlag != null && !request.getActivationFlags().contains(activationFlag)) { // allow approval if there is no flag, or if flag matches flags of activation @@ -231,7 +231,7 @@ public Response operationReject( @NotNull RequestContext requestContext, List activationFlags, String rejectReason) throws MobileTokenException, PowerAuthClientException { - final OperationDetailResponse operationDetail = getOperationDetail(operationId); + final OperationDetailResponse operationDetail = getOperationDetailInternal(operationId); final String activationFlag = operationDetail.getActivationFlag(); if (activationFlag != null && !activationFlags.contains(activationFlag)) { // allow approval if there is no flag, or if flag matches flags of activation @@ -265,6 +265,42 @@ public Response operationReject( } } + /** + * Get operation detail. + * + * @param operationId Operation ID. + * @param language Language. + * @param userId User identifier. + * @return Operation detail. + * @throws PowerAuthClientException In case communication with PowerAuth Server fails. + * @throws MobileTokenException In case the operation is in incorrect state. + * @throws MobileTokenConfigurationException In case operation template is not configured correctly. + */ + public Operation getOperationDetail(String operationId, String language, String userId) throws MobileTokenException, PowerAuthClientException, MobileTokenConfigurationException { + final OperationDetailResponse operationDetail = getOperationDetailInternal(operationId); + if (!userId.equals(operationDetail.getUserId())) { + logger.warn("User ID from operation does not match authenticated user ID."); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Invalid request"); + } + return convertOperation(language, operationDetail); + } + + /** + * Claim operation. + * + * @param operationId Operation ID. + * @param language Language. + * @param userId User identifier. + * @return Operation detail. + * @throws PowerAuthClientException In case communication with PowerAuth Server fails. + * @throws MobileTokenException In case the operation is in incorrect state. + * @throws MobileTokenConfigurationException In case operation template is not configured correctly. + */ + public Operation claimOperation(String operationId, String language, String userId) throws MobileTokenException, PowerAuthClientException, MobileTokenConfigurationException { + final OperationDetailResponse operationDetail = claimOperationInternal(operationId, userId); + return convertOperation(language, operationDetail); + } + // Private methods /** @@ -275,7 +311,7 @@ public Response operationReject( * @throws PowerAuthClientException In case communication with PowerAuth Server fails. * @throws MobileTokenException When the operation is in incorrect state. */ - private OperationDetailResponse getOperationDetail(String operationId) throws PowerAuthClientException, MobileTokenException { + private OperationDetailResponse getOperationDetailInternal(String operationId) throws PowerAuthClientException, MobileTokenException { final OperationDetailRequest operationDetailRequest = new OperationDetailRequest(); operationDetailRequest.setOperationId(operationId); final OperationDetailResponse operationDetail = powerAuthClient.operationDetail( @@ -287,6 +323,46 @@ private OperationDetailResponse getOperationDetail(String operationId) throws Po return operationDetail; } + /** + * Get operation detail by calling PowerAuth Server. + * + * @param operationId Operation ID. + * @param userId Optional user ID for operation claim. + * @return Operation detail. + * @throws PowerAuthClientException In case communication with PowerAuth Server fails. + * @throws MobileTokenException When the operation is in incorrect state. + */ + private OperationDetailResponse claimOperationInternal(String operationId, String userId) throws PowerAuthClientException, MobileTokenException { + final OperationDetailRequest operationDetailRequest = new OperationDetailRequest(); + operationDetailRequest.setOperationId(operationId); + operationDetailRequest.setUserId(userId); + final OperationDetailResponse operationDetail = powerAuthClient.operationDetail( + operationDetailRequest, + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + handleStatus(operationDetail); + return operationDetail; + } + + /** + * Find operation template and convert the operation. + * + * @param language Language. + * @param operationDetail Operation detail. + * @return Converted operation. + * @throws MobileTokenException In case the operation is in incorrect state. + * @throws MobileTokenConfigurationException In case operation template is not configured correctly. + */ + private Operation convertOperation(String language, OperationDetailResponse operationDetail) throws MobileTokenException, MobileTokenConfigurationException { + final Optional operationTemplate = operationTemplateService.findTemplate(operationDetail.getOperationType(), language); + if (operationTemplate.isEmpty()) { + logger.warn("Template not found for operationType={}.", operationDetail.getOperationType()); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Template not found"); + } + return mobileTokenConverter.convert(operationDetail, operationTemplate.get()); + } + /** * Handle operation status. * diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java index 08a2c6868..00c528aca 100644 --- a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java @@ -19,6 +19,7 @@ import com.wultra.security.powerauth.lib.mtoken.model.entity.PreApprovalScreen; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -33,7 +34,7 @@ @Data public class OperationApproveRequest { - @NotNull + @NotEmpty private String id; @NotNull private String data; diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationClaimRequest.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationClaimRequest.java new file mode 100644 index 000000000..44a55ff7e --- /dev/null +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationClaimRequest.java @@ -0,0 +1,37 @@ +/* + * PowerAuth Mobile Token Model + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.security.powerauth.lib.mtoken.model.request; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +/** + * Request for an operation detail. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@Data +public class OperationClaimRequest { + + @NotEmpty + private String id; + + @NotEmpty + private String userId; + +} diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationDetailRequest.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationDetailRequest.java new file mode 100644 index 000000000..406b73364 --- /dev/null +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationDetailRequest.java @@ -0,0 +1,34 @@ +/* + * PowerAuth Mobile Token Model + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.security.powerauth.lib.mtoken.model.request; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +/** + * Request for an operation detail. + * + * @author Roman Strobl, roman.strobl@wultra.com + */ +@Data +public class OperationDetailRequest { + + @NotEmpty + private String id; + +} diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationRejectRequest.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationRejectRequest.java index e6aa6c2ee..6e657401f 100644 --- a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationRejectRequest.java +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationRejectRequest.java @@ -17,7 +17,7 @@ */ package com.wultra.security.powerauth.lib.mtoken.model.request; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotEmpty; import lombok.Data; /** @@ -28,7 +28,7 @@ @Data public class OperationRejectRequest { - @NotNull + @NotEmpty private String id; private String reason; diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/PushRegisterRequest.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/PushRegisterRequest.java index aab7b5b8e..3c27b258d 100644 --- a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/PushRegisterRequest.java +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/PushRegisterRequest.java @@ -17,7 +17,7 @@ */ package com.wultra.security.powerauth.lib.mtoken.model.request; -import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotEmpty; import lombok.Data; /** @@ -28,9 +28,9 @@ @Data public class PushRegisterRequest { - @NotNull + @NotEmpty private String platform; - @NotNull + @NotEmpty private String token; } From 7b52f9d3185f5566fbe350a6ed2d1b545c99256e Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Tue, 5 Dec 2023 10:34:05 +0100 Subject: [PATCH 27/52] Fix #939: Custom Spring Boot Banner --- .../src/main/resources/application.properties | 3 +++ .../src/main/resources/banner.txt | 9 +++++++++ .../src/main/resources/application.properties | 3 +++ enrollment-server/src/main/resources/banner.txt | 9 +++++++++ 4 files changed, 24 insertions(+) create mode 100644 enrollment-server-onboarding/src/main/resources/banner.txt create mode 100644 enrollment-server/src/main/resources/banner.txt diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index a35d7ba1c..3cc2bcf5a 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -21,6 +21,9 @@ spring.profiles.active=ext spring.application.name=onboarding-server +banner.application.name=${spring.application.name} +banner.application.version=@project.version@ + # Database Configuration - PostgreSQL spring.datasource.url=jdbc:postgresql://localhost:5432/powerauth spring.datasource.username=powerauth diff --git a/enrollment-server-onboarding/src/main/resources/banner.txt b/enrollment-server-onboarding/src/main/resources/banner.txt new file mode 100644 index 000000000..964cb7bd0 --- /dev/null +++ b/enrollment-server-onboarding/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + ___ _ _ _ ____ + / _ \ _ __ | |__ ___ __ _ _ __ __| (_)_ __ __ _ / ___| ___ _ ____ _____ _ __ + | | | | '_ \| '_ \ / _ \ / _` | '__/ _` | | '_ \ / _` | \___ \ / _ \ '__\ \ / / _ \ '__| + | |_| | | | | |_) | (_) | (_| | | | (_| | | | | | (_| | ___) | __/ | \ V / __/ | + \___/|_| |_|_.__/ \___/ \__,_|_| \__,_|_|_| |_|\__, | |____/ \___|_| \_/ \___|_| + |___/ +${AnsiColor.GREEN} :: ${banner.application.name} (${banner.application.version}) :: ${AnsiColor.GREEN} +${AnsiColor.RED} :: Spring Boot${spring-boot.formatted-version} :: ${AnsiColor.RED} +${AnsiColor.DEFAULT} \ No newline at end of file diff --git a/enrollment-server/src/main/resources/application.properties b/enrollment-server/src/main/resources/application.properties index 76c0634e0..c16c6f2b3 100644 --- a/enrollment-server/src/main/resources/application.properties +++ b/enrollment-server/src/main/resources/application.properties @@ -21,6 +21,9 @@ spring.profiles.active=ext spring.application.name=enrollment-server +banner.application.name=${spring.application.name} +banner.application.version=@project.version@ + # Database Configuration - PostgreSQL spring.datasource.url=jdbc:postgresql://localhost:5432/powerauth spring.datasource.username=powerauth diff --git a/enrollment-server/src/main/resources/banner.txt b/enrollment-server/src/main/resources/banner.txt new file mode 100644 index 000000000..b1696bc76 --- /dev/null +++ b/enrollment-server/src/main/resources/banner.txt @@ -0,0 +1,9 @@ + _____ _ _ _ ____ + | ____|_ __ _ __ ___ | | |_ __ ___ ___ _ __ | |_ / ___| ___ _ ____ _____ _ __ + | _| | '_ \| '__/ _ \| | | '_ ` _ \ / _ \ '_ \| __| \___ \ / _ \ '__\ \ / / _ \ '__| + | |___| | | | | | (_) | | | | | | | | __/ | | | |_ ___) | __/ | \ V / __/ | + |_____|_| |_|_| \___/|_|_|_| |_| |_|\___|_| |_|\__| |____/ \___|_| \_/ \___|_| + +${AnsiColor.GREEN} :: ${banner.application.name} (${banner.application.version}) :: ${AnsiColor.GREEN} +${AnsiColor.RED} :: Spring Boot${spring-boot.formatted-version} :: ${AnsiColor.RED} +${AnsiColor.DEFAULT} \ No newline at end of file From 7529debee403c63131c36b77578ee6d2e1718d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Tue, 5 Dec 2023 14:41:43 +0100 Subject: [PATCH 28/52] Fix #908: Innovatrics PresenceCheckProvider (#936) * Fix #908: Innovatrics PresenceCheckProvider --- docs/onboarding/Configuration-Properties.md | 2 + .../DocumentVerificationProvider.java | 9 + .../api/provider/PresenceCheckProvider.java | 12 +- .../innovatrics/InnovatricsApiService.java | 94 +++++++++- .../innovatrics/InnovatricsConfigProps.java | 10 ++ ...novatricsDocumentVerificationProvider.java | 5 + .../InnovatricsPresenceCheckProvider.java | 130 +++++++++++++- .../InnovatricsPresenceCheckProviderTest.java | 160 ++++++++++++++++++ .../iproov/IProovPresenceCheckProvider.java | 9 +- .../ZenidDocumentVerificationProvider.java | 5 + ...ultraMockDocumentVerificationProvider.java | 5 + .../impl/service/PresenceCheckService.java | 91 ++++++---- .../document/DocumentProcessingService.java | 4 + .../WultraMockPresenceCheckProvider.java | 7 +- .../src/main/resources/application.properties | 5 +- .../service/PresenceCheckServiceTest.java | 61 ++++--- .../WultraMockPresenceCheckProviderTest.java | 2 +- 17 files changed, 534 insertions(+), 77 deletions(-) create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index 330412aaa..cbd0952d0 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -142,6 +142,7 @@ The Onboarding Server uses the following public configuration properties: | `enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl` | | Base REST service URL for Innovatrics. | | `enrollment-server-onboarding.provider.innovatrics.serviceToken` | | Authentication token for Innovatrics. | | `enrollment-server-onboarding.provider.innovatrics.serviceUserAgent` | `Wultra/OnboardingServer` | User agent to use when making HTTP calls to Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.presenceCheck.score` | 0.875 | Presence check minimal score threshold. | | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate` | `false` | Whether invalid SSL certificate is accepted when calling Zen ID REST service. | | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling Innovatrics REST service. | | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling Innovatrics REST service. | @@ -150,6 +151,7 @@ The Onboarding Server uses the following public configuration properties: | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyUsername` | | Proxy username to be used when calling Innovatrics REST service. | | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPassword` | | Proxy password to be used when calling Innovatrics REST service. | +See [Innovatrics documentation](https://developers.innovatrics.com/digital-onboarding/docs/functionalities/face/active-liveness-check/#magnifeye-liveness) for details how the score affects false acceptances (FAR) and false rejections (FRR). ## Correlation HTTP Header Configuration diff --git a/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/DocumentVerificationProvider.java b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/DocumentVerificationProvider.java index 60ceb556f..dfbfb5e41 100644 --- a/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/DocumentVerificationProvider.java +++ b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/DocumentVerificationProvider.java @@ -58,6 +58,15 @@ public interface DocumentVerificationProvider { */ DocumentsSubmitResult submitDocuments(OwnerId id, List documents) throws RemoteCommunicationException, DocumentVerificationException; + /** + * A feature flag whether the selfie result gained by {@link PresenceCheckProvider} should be stored by {@link #submitDocuments(OwnerId, List)}. + *

    + * Some implementation may require this cross-sending between providers by the Onboarding server, some providers may handle it internally. + * + * @return {@code true} if cross-sending between providers should be handled by Onboarding server, {@code false} otherwise. + */ + boolean shouldStoreSelfie(); + /** * Analyze previously submitted documents, detect frauds, return binary result * diff --git a/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java index f8d2af938..298b5296b 100644 --- a/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java +++ b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java @@ -43,6 +43,15 @@ public interface PresenceCheckProvider { */ void initPresenceCheck(OwnerId id, Image photo) throws PresenceCheckException, RemoteCommunicationException; + /** + * A feature flag whether the trusted photo of the user should be passed to {@link #initPresenceCheck(OwnerId, Image)}. + *

    + * Some implementation may require specific source to be called by Onboarding server, some providers may handle it internally. + * + * @return {@code true} if the trusted photo should be provided, {@code false} otherwise. + */ + boolean shouldProvideTrustedPhoto(); + /** * Starts the presence check process. The process has to be initialized before this call. * @@ -67,9 +76,10 @@ public interface PresenceCheckProvider { * Cleans up all presence check data related to the identity. * * @param id Owner identification. + * @param sessionInfo Session info with presence check relevant data. * @throws PresenceCheckException In case of business logic error. * @throws RemoteCommunicationException In case of remote communication error. */ - void cleanupIdentityData(OwnerId id) throws PresenceCheckException, RemoteCommunicationException; + void cleanupIdentityData(OwnerId id, SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException; } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java index 92260f9b8..f4a6a2089 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java @@ -17,9 +17,13 @@ */ package com.wultra.app.onboardingserver.provider.innovatrics; +import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CustomerInspectResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessRequest; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessResponse; import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientException; -import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -27,6 +31,8 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; /** * Implementation of the REST service toInnovatrics. @@ -43,7 +49,9 @@ @Slf4j class InnovatricsApiService { - private static final ParameterizedTypeReference STRING_TYPE_REFERENCE = new ParameterizedTypeReference<>() { }; + private static final MultiValueMap EMPTY_ADDITIONAL_HEADERS = new LinkedMultiValueMap<>(); + + private static final MultiValueMap EMPTY_QUERY_PARAMS = new LinkedMultiValueMap<>(); /** * REST client for Innovatrics calls. @@ -60,11 +68,81 @@ public InnovatricsApiService(@Qualifier("restClientInnovatrics") final RestClien this.restClient = restClient; } - // TODO remove - temporal test call - @PostConstruct - public void testCall() throws RestClientException { - logger.info("Trying a test call"); - final ResponseEntity response = restClient.get("/api/v1/metadata", STRING_TYPE_REFERENCE); - logger.info("Result of test call: {}", response.getBody()); + public EvaluateCustomerLivenessResponse evaluateLiveness(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final EvaluateCustomerLivenessRequest request = new EvaluateCustomerLivenessRequest(); + request.setType(EvaluateCustomerLivenessRequest.TypeEnum.MAGNIFEYE_LIVENESS); + + final String apiPath = "/api/v1/customers/%s/liveness/evaluation".formatted(customerId); + + try { + logger.info("Calling liveness/evaluation, {}", ownerId); + logger.debug("Calling {}, {}", apiPath, request); + final ResponseEntity response = restClient.post(apiPath, request, EMPTY_QUERY_PARAMS, EMPTY_ADDITIONAL_HEADERS, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for liveness/evaluation, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException( + String.format("Failed REST call to evaluate liveness for customerId=%s, statusCode=%s, responseBody='%s'", customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when evaluating liveness for customerId=" + customerId, e); + } } + + public CustomerInspectResponse inspectCustomer(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/inspect".formatted(customerId); + + try { + logger.info("Calling /inspect, {}", ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.post(apiPath, null, EMPTY_QUERY_PARAMS, EMPTY_ADDITIONAL_HEADERS, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for /inspect, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException( + String.format("Failed REST call to evaluate inspect for customerId=%s, statusCode=%s, responseBody='%s'", customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when evaluating inspect for customerId=" + customerId, e); + } + } + + public void deleteLiveness(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/liveness".formatted(customerId); + + try { + logger.info("Deleting liveness, {}", ownerId); + logger.debug("Deleting {}", apiPath); + final ResponseEntity response = restClient.delete(apiPath, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for liveness delete, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + } catch (RestClientException e) { + throw new RemoteCommunicationException( + String.format("Failed REST call to delete liveness for customerId=%s, statusCode=%s, responseBody='%s'", customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when deleting liveness for customerId=" + customerId, e); + } + } + + public void deleteSelfie(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/selfie".formatted(customerId); + + try { + logger.info("Deleting selfie, {}", ownerId); + logger.debug("Deleting {}", apiPath); + final ResponseEntity response = restClient.delete(apiPath, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for selfie delete, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + } catch (RestClientException e) { + throw new RemoteCommunicationException( + String.format("Failed REST call to delete selfie for customerId=%s, statusCode=%s, responseBody='%s'", customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when deleting selfie for customerId=" + customerId, e); + } + } + } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java index b12c4970c..62fb7f67c 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java @@ -60,4 +60,14 @@ class InnovatricsConfigProps { */ private RestClientConfiguration restClientConfig; + private PresenceCheckConfiguration presenceCheckConfiguration; + + @Getter @Setter + public static class PresenceCheckConfiguration { + /** + * Presence check minimal score threshold. + */ + private double score = 0.875; + } + } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java index c7bb40bed..70d177e73 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java @@ -48,6 +48,11 @@ public DocumentsSubmitResult submitDocuments(OwnerId id, List return null; } + @Override + public boolean shouldStoreSelfie() { + return false; + } + @Override public DocumentsVerificationResult verifyDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException { return null; diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java index 1c7c65392..7899e776c 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java @@ -17,6 +17,8 @@ */ package com.wultra.app.onboardingserver.provider.innovatrics; +import com.google.common.base.Strings; +import com.wultra.app.enrollmentserver.model.enumeration.PresenceCheckStatus; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult; @@ -24,35 +26,151 @@ import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CustomerInspectResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.SelfieSimilarityWith; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; +import java.util.Locale; +import java.util.Optional; + /** * Implementation of the {@link PresenceCheckProvider} with Innovatrics. * * @author Jan Pesek, jan.pesek@wultra.com + * @author Lubos Racansky, lubos.racansky@wultra.com */ @ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "innovatrics") @Component +@Slf4j +@AllArgsConstructor class InnovatricsPresenceCheckProvider implements PresenceCheckProvider { + private static final String INNOVATRICS_CUSTOMER_ID = "InnovatricsCustomerId"; + + private final InnovatricsApiService innovatricsApiService; + + private final InnovatricsConfigProps configuration; + @Override - public void initPresenceCheck(OwnerId id, Image photo) throws PresenceCheckException, RemoteCommunicationException { + public void initPresenceCheck(final OwnerId id, final Image photo) { + logger.debug("#initPresenceCheck does nothing for Innovatrics, {}", id); + } + @Override + public boolean shouldProvideTrustedPhoto() { + return false; } @Override - public SessionInfo startPresenceCheck(OwnerId id) throws PresenceCheckException, RemoteCommunicationException { - return null; + public SessionInfo startPresenceCheck(final OwnerId id) { + logger.debug("#startPresenceCheck does nothing for Innovatrics, {}", id); + return new SessionInfo(); } @Override - public PresenceCheckResult getResult(OwnerId id, SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException { - return null; + public PresenceCheckResult getResult(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException { + final String customerId = fetchCustomerId(id, sessionInfo); + + final Optional evaluateLivenessError = evaluateLiveness(customerId, id); + if (evaluateLivenessError.isPresent()) { + return convert(evaluateLivenessError.get()); + } + logger.debug("Liveness passed, {}", id); + + // do not be afraid of the timing attack, the action is invoked by the state machine, not by the user + final Optional inspectCustomerError = inspectCustomer(customerId, id); + if (inspectCustomerError.isPresent()) { + return convert(inspectCustomerError.get()); + } + logger.debug("Customer inspection passed, {}", id); + + return accepted(); + } + + private static PresenceCheckResult accepted() { + final PresenceCheckResult result = new PresenceCheckResult(); + result.setStatus(PresenceCheckStatus.ACCEPTED); + return result; + } + + private static PresenceCheckResult convert(final PresenceCheckError source) { + final PresenceCheckResult target = new PresenceCheckResult(); + target.setStatus(source.status()); + target.setErrorDetail(source.errorDetail()); + target.setRejectReason(source.rejectReason()); + return target; + } + + private Optional evaluateLiveness(final String customerId, final OwnerId id) throws RemoteCommunicationException { + final EvaluateCustomerLivenessResponse livenessResponse = innovatricsApiService.evaluateLiveness(customerId, id); + final Double score = livenessResponse.getScore(); + final EvaluateCustomerLivenessResponse.ErrorCodeEnum errorCode = livenessResponse.getErrorCode(); + logger.debug("Presence check score: {}, errorCode: {}, {}", score, errorCode, id); + final double scoreThreshold = configuration.getPresenceCheckConfiguration().getScore(); + + if (score == null) { + return fail(errorCode == null ? "Score is null" : errorCode.getValue()); + } else if (score < scoreThreshold) { + return reject(String.format(Locale.ENGLISH, "Score %.3f is bellow the threshold %.3f", score, scoreThreshold)); + } else { + return success(); + } + } + + private Optional inspectCustomer(final String customerId, final OwnerId id) throws RemoteCommunicationException{ + final CustomerInspectResponse customerInspectResponse = innovatricsApiService.inspectCustomer(customerId, id); + + if (customerInspectResponse.getSelfieInspection() == null || customerInspectResponse.getSelfieInspection().getSimilarityWith() == null) { + return fail("Missing selfie inspection payload"); + } + + final SelfieSimilarityWith similarityWith = customerInspectResponse.getSelfieInspection().getSimilarityWith(); + + if (!Boolean.TRUE.equals(similarityWith.getLivenessSelfies())) { + return reject("The person in the selfie does not match a person in each liveness selfie"); + } else if (!Boolean.TRUE.equals(similarityWith.getDocumentPortrait())) { + return reject("The person in the selfie does not match a person in the document portrait"); + } else { + return success(); + } + } + + private static Optional success() { + return Optional.empty(); + } + + private static Optional reject(final String rejectReason) { + return Optional.of(new PresenceCheckError(PresenceCheckStatus.REJECTED, rejectReason, null)); + } + + private static Optional fail(final String errorDetail) { + return Optional.of(new PresenceCheckError(PresenceCheckStatus.FAILED, null, errorDetail)); + } + + private static String fetchCustomerId(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException { + // TODO (racansky, 2023-11-28) discuss the format with Jan Pesek + final String customerId = (String) sessionInfo.getSessionAttributes().get(INNOVATRICS_CUSTOMER_ID); + if (Strings.isNullOrEmpty(customerId)) { + throw new PresenceCheckException("Missing a customer ID value for calling Innovatrics, " + id); + } + return customerId; } @Override - public void cleanupIdentityData(OwnerId id) throws PresenceCheckException, RemoteCommunicationException { + public void cleanupIdentityData(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException { + logger.info("Invoked cleanupIdentityData, {}", id); + final String customerId = fetchCustomerId(id, sessionInfo); + + innovatricsApiService.deleteLiveness(customerId, id); + logger.debug("Deleted liveness, {}", id); + innovatricsApiService.deleteSelfie(customerId, id); + logger.debug("Deleted selfie, {}", id); } + + record PresenceCheckError(PresenceCheckStatus status, String rejectReason, String errorDetail){} } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java new file mode 100644 index 000000000..7037ca408 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java @@ -0,0 +1,160 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.app.enrollmentserver.model.enumeration.PresenceCheckStatus; +import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.enrollmentserver.model.integration.PresenceCheckResult; +import com.wultra.app.enrollmentserver.model.integration.SessionInfo; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CustomerInspectResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.SelfieInspection; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.SelfieSimilarityWith; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.when; + +/** + * Test for {@link InnovatricsPresenceCheckProvider}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ExtendWith(MockitoExtension.class) +class InnovatricsPresenceCheckProviderTest { + + private static final String CUSTOMER_ID = "customer-1"; + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private InnovatricsConfigProps innovatricsConfigProps; + + @Mock + private InnovatricsApiService innovatricsApiService; + + @InjectMocks + private InnovatricsPresenceCheckProvider tested; + + @Test + void testGetResult_success() throws Exception { + final OwnerId id = new OwnerId(); + final SessionInfo sessionInfo = createSessionInfo(); + + when(innovatricsApiService.evaluateLiveness(CUSTOMER_ID, id)) + .thenReturn(new EvaluateCustomerLivenessResponse(0.95, null)); + when(innovatricsConfigProps.getPresenceCheckConfiguration().getScore()) + .thenReturn(0.80); + when(innovatricsApiService.inspectCustomer(CUSTOMER_ID, id)) + .thenReturn(createCustomerInspectResponse(true, true)); + + final PresenceCheckResult result = tested.getResult(id, sessionInfo); + + assertEquals(PresenceCheckStatus.ACCEPTED, result.getStatus()); + assertNull(result.getErrorDetail()); + assertNull(result.getRejectReason()); + } + + @Test + void testGetResult_livenessFailed() throws Exception { + final OwnerId id = new OwnerId(); + final SessionInfo sessionInfo = createSessionInfo(); + + when(innovatricsApiService.evaluateLiveness(CUSTOMER_ID, id)) + .thenReturn(new EvaluateCustomerLivenessResponse(null, EvaluateCustomerLivenessResponse.ErrorCodeEnum.NOT_ENOUGH_DATA)); + + final PresenceCheckResult result = tested.getResult(id, sessionInfo); + + assertEquals(PresenceCheckStatus.FAILED, result.getStatus()); + assertEquals("NOT_ENOUGH_DATA", result.getErrorDetail()); + assertNull(result.getRejectReason()); + } + + @Test + void testGetResult_livenessRejected() throws Exception { + final OwnerId id = new OwnerId(); + final SessionInfo sessionInfo = createSessionInfo(); + + when(innovatricsApiService.evaluateLiveness(CUSTOMER_ID, id)) + .thenReturn(new EvaluateCustomerLivenessResponse(0.70, null)); + when(innovatricsConfigProps.getPresenceCheckConfiguration().getScore()) + .thenReturn(0.875); + + final PresenceCheckResult result = tested.getResult(id, sessionInfo); + + assertEquals(PresenceCheckStatus.REJECTED, result.getStatus()); + assertNull(result.getErrorDetail()); + assertEquals("Score 0.700 is bellow the threshold 0.875", result.getRejectReason()); + } + + @Test + void testGetResult_customerInspectionFailed() throws Exception { + final OwnerId id = new OwnerId(); + final SessionInfo sessionInfo = createSessionInfo(); + + when(innovatricsApiService.evaluateLiveness(CUSTOMER_ID, id)) + .thenReturn(new EvaluateCustomerLivenessResponse(0.95, null)); + when(innovatricsConfigProps.getPresenceCheckConfiguration().getScore()) + .thenReturn(0.80); + when(innovatricsApiService.inspectCustomer(CUSTOMER_ID, id)) + .thenReturn(new CustomerInspectResponse()); // selfieInspection == null + + final PresenceCheckResult result = tested.getResult(id, sessionInfo); + + assertEquals(PresenceCheckStatus.FAILED, result.getStatus()); + assertEquals("Missing selfie inspection payload", result.getErrorDetail()); + assertNull(result.getRejectReason()); + } + + @Test + void testGetResult_customerInspectionRejected() throws Exception { + final OwnerId id = new OwnerId(); + final SessionInfo sessionInfo = createSessionInfo(); + + when(innovatricsApiService.evaluateLiveness(CUSTOMER_ID, id)) + .thenReturn(new EvaluateCustomerLivenessResponse(0.95, null)); + when(innovatricsConfigProps.getPresenceCheckConfiguration().getScore()) + .thenReturn(0.80); + when(innovatricsApiService.inspectCustomer(CUSTOMER_ID, id)) + .thenReturn(createCustomerInspectResponse(false, true)); + + final PresenceCheckResult result = tested.getResult(id, sessionInfo); + + assertEquals(PresenceCheckStatus.REJECTED, result.getStatus()); + assertNull(result.getErrorDetail()); + assertEquals("The person in the selfie does not match a person in the document portrait", result.getRejectReason()); + } + + private static SessionInfo createSessionInfo() { + final SessionInfo sessionInfo = new SessionInfo(); + sessionInfo.setSessionAttributes(Map.of("InnovatricsCustomerId", CUSTOMER_ID)); + return sessionInfo; + } + + private static CustomerInspectResponse createCustomerInspectResponse(final Boolean documentPortrait, final Boolean livenessSelfies) { + return new CustomerInspectResponse() + .selfieInspection(new SelfieInspection() + .similarityWith(new SelfieSimilarityWith(documentPortrait, livenessSelfies))); + } +} \ No newline at end of file diff --git a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java index aa15ce1b4..8e85e5f66 100644 --- a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java +++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java @@ -40,6 +40,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.util.Assert; import java.util.Base64; @@ -80,6 +81,7 @@ public IProovPresenceCheckProvider( @Override public void initPresenceCheck(final OwnerId id, final Image photo) throws PresenceCheckException, RemoteCommunicationException { + Assert.notNull(photo, "iProov presence check requires trusted photo"); iProovRestApiService.deleteUserIfAlreadyExists(id); final ResponseEntity responseEntityToken = callGenerateEnrolToken(id); @@ -124,6 +126,11 @@ public void initPresenceCheck(final OwnerId id, final Image photo) throws Presen } } + @Override + public boolean shouldProvideTrustedPhoto() { + return true; + } + @Override public SessionInfo startPresenceCheck(OwnerId id) throws PresenceCheckException, RemoteCommunicationException { final ResponseEntity responseEntity; @@ -210,7 +217,7 @@ private static PresenceCheckResult convert(final ClaimValidateResponse source, f } @Override - public void cleanupIdentityData(final OwnerId id) { + public void cleanupIdentityData(final OwnerId id, final SessionInfo sessionInfo) { // https://docs.iproov.com/docs/Content/ImplementationGuide/security/data-retention.htm logger.info("No data deleted, retention policy left to iProov server, {}", id); } diff --git a/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProvider.java index 6f766b971..012d48715 100644 --- a/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProvider.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidDocumentVerificationProvider.java @@ -178,6 +178,11 @@ public DocumentsSubmitResult submitDocuments(OwnerId id, List return result; } + @Override + public boolean shouldStoreSelfie() { + return true; + } + @Override public DocumentsVerificationResult verifyDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException { ResponseEntity responseEntity; diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java index 539e9b36f..bddd9e43c 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java @@ -122,6 +122,11 @@ public DocumentsSubmitResult submitDocuments(OwnerId id, List return result; } + @Override + public boolean shouldStoreSelfie() { + return true; + } + @Override public DocumentsVerificationResult verifyDocuments(OwnerId id, List uploadIds) { final String verificationId = UUID.randomUUID().toString(); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java index 814e8fd7a..86323a35f 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java @@ -21,6 +21,9 @@ import com.google.common.base.Preconditions; import com.wultra.app.enrollmentserver.model.enumeration.*; import com.wultra.app.enrollmentserver.model.integration.*; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; +import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; import com.wultra.app.onboardingserver.common.database.ScaResultRepository; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; @@ -31,16 +34,12 @@ import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; import com.wultra.app.onboardingserver.common.service.AuditService; import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; -import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.api.errorhandling.PresenceCheckException; import com.wultra.app.onboardingserver.errorhandling.PresenceCheckLimitException; import com.wultra.app.onboardingserver.impl.service.document.DocumentProcessingService; import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService; -import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -60,10 +59,9 @@ */ @Service @AllArgsConstructor +@Slf4j public class PresenceCheckService { - private static final Logger logger = LoggerFactory.getLogger(PresenceCheckService.class); - private static final String SESSION_ATTRIBUTE_TIMESTAMP_LAST_USED = "timestampLastUsed"; private static final String SESSION_ATTRIBUTE_IMAGE_UPLOADED = "imageUploaded"; @@ -141,13 +139,20 @@ public void checkPresenceVerification( logger.debug("Processing a result of an accepted presence check, {}", ownerId); } - // Process the photo irrespective of the result status - final Image photo = result.getPhoto(); - if (photo == null) { - evaluatePresenceCheckResult(ownerId, idVerification, result); - throw new PresenceCheckException("Missing person photo from presence verification, " + ownerId); + if (!documentProcessingService.shouldDocumentProviderStoreSelfie()) { + logger.debug("Selfie will not be submitted to document provider, {}", ownerId); + } else if (result.getPhoto() == null) { + logger.warn("Missing person photo from presence verification, {}", ownerId); + } else { + logger.debug("Obtained a person photo from the presence verification, {}", ownerId); + submitSelfiePhoto(ownerId, idVerification, result); } - logger.debug("Obtained a photo from the result, {}", ownerId); + + evaluatePresenceCheckResult(ownerId, idVerification, result); + } + + private void submitSelfiePhoto(final OwnerId ownerId, final IdentityVerificationEntity idVerification, final PresenceCheckResult result) { + final Image photo = result.getPhoto(); final SubmittedDocument submittedDoc = new SubmittedDocument(); // TODO use different random id approach @@ -157,22 +162,20 @@ public void checkPresenceVerification( submittedDoc.setPhoto(photo); submittedDoc.setType(DocumentType.SELFIE_PHOTO); - DocumentVerificationEntity docVerificationEntity = new DocumentVerificationEntity(); + final DocumentVerificationEntity docVerificationEntity = new DocumentVerificationEntity(); docVerificationEntity.setActivationId(ownerId.getActivationId()); docVerificationEntity.setIdentityVerification(idVerification); - docVerificationEntity.setFilename(result.getPhoto().getFilename()); + docVerificationEntity.setFilename(photo.getFilename()); docVerificationEntity.setTimestampCreated(ownerId.getTimestamp()); docVerificationEntity.setType(DocumentType.SELFIE_PHOTO); docVerificationEntity.setUsedForVerification(identityVerificationConfig.isVerifySelfieWithDocumentsEnabled()); - DocumentSubmitResult documentSubmitResult = + final DocumentSubmitResult documentSubmitResult = documentProcessingService.submitDocumentToProvider(ownerId, docVerificationEntity, submittedDoc); docVerificationEntity.setTimestampUploaded(ownerId.getTimestamp()); docVerificationEntity.setUploadId(documentSubmitResult.getUploadId()); documentVerificationRepository.save(docVerificationEntity); - - evaluatePresenceCheckResult(ownerId, idVerification, result); } /** @@ -184,9 +187,9 @@ public void checkPresenceVerification( */ public void cleanup(OwnerId ownerId) throws PresenceCheckException, RemoteCommunicationException { if (identityVerificationConfig.isPresenceCheckCleanupEnabled()) { - presenceCheckProvider.cleanupIdentityData(ownerId); final IdentityVerificationEntity identityVerification = identityVerificationService.findByOptional(ownerId).orElseThrow(() -> new PresenceCheckException("Unable to find identity verification for " + ownerId)); + presenceCheckProvider.cleanupIdentityData(ownerId, deserializeSessionInfo(identityVerification, ownerId)); auditService.auditPresenceCheckProvider(identityVerification, "Clean up presence check data for user: {}", ownerId.getUserId()); } else { logger.debug("Skipped cleanup of presence check data at the provider (not enabled), {}", ownerId); @@ -198,7 +201,7 @@ public void cleanup(OwnerId ownerId) throws PresenceCheckException, RemoteCommun * * @param ownerId Owner identification. * @param idVerification Verification identity. - * @throws DocumentVerificationException When not able to find documet image. + * @throws DocumentVerificationException When not able to find document image. * @throws PresenceCheckException In case of business logic error. * @throws RemoteCommunicationException In case of remote communication error. */ @@ -212,17 +215,21 @@ private void initPresentCheckWithImage(final OwnerId ownerId, final IdentityVeri return; } - final List docsWithPhoto = documentVerificationRepository.findAllWithPhoto(idVerification); - if (docsWithPhoto.isEmpty()) { - throw new PresenceCheckException("Unable to initialize presence check - missing person photo, " + ownerId); - } else { - final Image photo = selectPhotoForPresenceCheck(ownerId, docsWithPhoto); - final Image upscaledPhoto = imageProcessor.upscaleImage(ownerId, photo, identityVerificationConfig.getMinimalSelfieWidth()); - presenceCheckProvider.initPresenceCheck(ownerId, upscaledPhoto); - logger.info("Presence check initialized, {}", ownerId); - updateSessionInfo(ownerId, idVerification, Map.of(SESSION_ATTRIBUTE_IMAGE_UPLOADED, true)); - auditService.auditPresenceCheckProvider(idVerification, "Presence check initialized for user: {}", ownerId.getUserId()); - } + final Optional photo = fetchTrustedPhoto(ownerId, idVerification); + presenceCheckProvider.initPresenceCheck(ownerId, photo.orElse(null)); + logger.info("Presence check initialized, {}", ownerId); + updateSessionInfo(ownerId, idVerification, Map.of(SESSION_ATTRIBUTE_IMAGE_UPLOADED, true)); + auditService.auditPresenceCheckProvider(idVerification, "Presence check initialized for user: {}", ownerId.getUserId()); + } + } + + private Optional fetchTrustedPhoto(final OwnerId ownerId, final IdentityVerificationEntity idVerification) throws DocumentVerificationException, RemoteCommunicationException, PresenceCheckException { + if (presenceCheckProvider.shouldProvideTrustedPhoto()) { + final Image photo = fetchTrustedPhotoFromDocumentVerifier(ownerId, idVerification); + final Image upscaledPhoto = imageProcessor.upscaleImage(ownerId, photo, identityVerificationConfig.getMinimalSelfieWidth()); + return Optional.of(upscaledPhoto); + } else { + return Optional.empty(); } } @@ -249,12 +256,19 @@ private SessionInfo startPresenceCheck(OwnerId ownerId, IdentityVerificationEnti /** * Selects person photo for the presence check process * @param ownerId Owner identification. - * @param docsWithPhoto Documents with a mined person photography. + * @param idVerification Verification identity. * @return Image with a person photography * @throws RemoteCommunicationException In case of remote communication error. * @throws DocumentVerificationException In case of business logic error. */ - protected Image selectPhotoForPresenceCheck(OwnerId ownerId, List docsWithPhoto) throws DocumentVerificationException, RemoteCommunicationException { + protected Image fetchTrustedPhotoFromDocumentVerifier(final OwnerId ownerId, final IdentityVerificationEntity idVerification) + throws DocumentVerificationException, RemoteCommunicationException { + + final List docsWithPhoto = documentVerificationRepository.findAllWithPhoto(idVerification); + if (docsWithPhoto.isEmpty()) { + throw new DocumentVerificationException("Unable to initialize presence check - missing person photo, " + ownerId); + } + docsWithPhoto.forEach(docWithPhoto -> Preconditions.checkNotNull(docWithPhoto.getPhotoId(), "Expected photoId value in " + docWithPhoto) ); @@ -262,7 +276,7 @@ protected Image selectPhotoForPresenceCheck(OwnerId ownerId, List docEntity = docsWithPhoto.stream() - .filter(value -> documentType.equals(value.getType())) + .filter(value -> value.getType() == documentType) .findFirst(); if (docEntity.isPresent()) { preferredDocWithPhoto = docEntity.get(); @@ -352,13 +366,18 @@ private IdentityVerificationEntity fetchIdVerification(OwnerId ownerId) throws P } private SessionInfo updateSessionInfo(final OwnerId ownerId, final IdentityVerificationEntity identityVerification, final Map sessionAttributes) throws PresenceCheckException { + final SessionInfo sessionInfo = deserializeSessionInfo(identityVerification, ownerId); + sessionInfo.getSessionAttributes().putAll(sessionAttributes); + identityVerification.setSessionInfo(jsonSerializationService.serialize(sessionInfo)); + return sessionInfo; + } + + private SessionInfo deserializeSessionInfo(final IdentityVerificationEntity identityVerification, final OwnerId ownerId) throws PresenceCheckException { final String sessionInfoString = StringUtils.defaultIfEmpty(identityVerification.getSessionInfo(), "{}"); final SessionInfo sessionInfo = jsonSerializationService.deserialize(sessionInfoString, SessionInfo.class); if (sessionInfo == null) { throw new PresenceCheckException("Unable to parse SessionInfo, identity verification ID: %s, %s".formatted(identityVerification.getId(), ownerId)); } - sessionInfo.getSessionAttributes().putAll(sessionAttributes); - identityVerification.setSessionInfo(jsonSerializationService.serialize(sessionInfo)); return sessionInfo; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java index 5cd96d745..948919729 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java @@ -263,6 +263,10 @@ public DocumentSubmitResult submitDocumentToProvider(OwnerId ownerId, DocumentVe return docSubmitResult; } + public boolean shouldDocumentProviderStoreSelfie() { + return documentVerificationProvider.shouldStoreSelfie(); + } + /** * Upload a single document related to identity verification. * @param idVerification Identity verification entity. diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java index 797d5212d..a42241794 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java @@ -61,6 +61,11 @@ public void initPresenceCheck(OwnerId id, Image photo) { logger.info("Mock - initialized presence check with a photo, {}", id); } + @Override + public boolean shouldProvideTrustedPhoto() { + return true; + } + @Override public SessionInfo startPresenceCheck(OwnerId id) { String token = UUID.randomUUID().toString(); @@ -101,7 +106,7 @@ private static byte[] readImage() { } @Override - public void cleanupIdentityData(OwnerId id) { + public void cleanupIdentityData(final OwnerId id, final SessionInfo sessionInfo) { logger.info("Mock - cleaned up identity data, {}", id); } diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index a35d7ba1c..45255f2e5 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -164,11 +164,14 @@ enrollment-server-onboarding.presence-check.iproov.restClientConfig.connectionTi enrollment-server-onboarding.presence-check.iproov.restClientConfig.responseTimeout=60000 enrollment-server-onboarding.presence-check.iproov.restClientConfig.maxIdleTime=200s -# Innovatrics configuration +# Innovatrics common configuration enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl=${INNOVATRICS_SERVICE_BASE_URL} enrollment-server-onboarding.provider.innovatrics.serviceToken=${INNOVATRICS_SERVICE_TOKEN} enrollment-server-onboarding.provider.innovatrics.serviceUserAgent=Wultra/OnboardingServer +# Innovatrics presence-check configuration +enrollment-server-onboarding.provider.innovatrics.presenceCheck.score=0.875 + # Innovatrics REST client configuration enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate=false enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize=10485760 diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java index 3bef254f5..c5daedb85 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java @@ -16,64 +16,81 @@ */ package com.wultra.app.onboardingserver.impl.service; -import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; -import org.junit.jupiter.api.BeforeEach; +import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; +import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; +import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.junit.jupiter.MockitoExtension; import java.util.List; import static org.mockito.Mockito.*; /** + * Test for {@link PresenceCheckService}. + * * @author Lukas Lukovsky, lukas.lukovsky@wultra.com + * @author Lubos Racansky, lubos.racansky@wultra.com */ +@ExtendWith(MockitoExtension.class) class PresenceCheckServiceTest { @Mock private IdentityVerificationService identityVerificationService; - @InjectMocks - private PresenceCheckService service; + @Mock + private DocumentVerificationRepository documentVerificationRepository; - @BeforeEach - void init() { - MockitoAnnotations.openMocks(this); - } + @InjectMocks + private PresenceCheckService tested; @Test - void selectPhotoForPresenceCheckTest() throws Exception { - OwnerId ownerId = new OwnerId(); + void testFetchTrustedPhotoFromDocumentVerifier_reverseOrder() throws Exception { + final OwnerId ownerId = new OwnerId(); + final IdentityVerificationEntity identityVerification = new IdentityVerificationEntity(); - // Two documents with person photo in reversed order of preference - DocumentVerificationEntity docPhotoDrivingLicense = new DocumentVerificationEntity(); + final DocumentVerificationEntity docPhotoDrivingLicense = new DocumentVerificationEntity(); docPhotoDrivingLicense.setPhotoId("drivingLicensePhotoId"); docPhotoDrivingLicense.setType(DocumentType.DRIVING_LICENSE); - DocumentVerificationEntity docPhotoIdCard = new DocumentVerificationEntity(); + final DocumentVerificationEntity docPhotoIdCard = new DocumentVerificationEntity(); docPhotoIdCard.setPhotoId("idCardPhotoId"); docPhotoIdCard.setType(DocumentType.ID_CARD); - List documentsReversedOrder = List.of(docPhotoDrivingLicense, docPhotoIdCard); + final List documentsReversedOrder = List.of(docPhotoDrivingLicense, docPhotoIdCard); + + when(documentVerificationRepository.findAllWithPhoto(identityVerification)) + .thenReturn(documentsReversedOrder); + when(identityVerificationService.getPhotoById(docPhotoIdCard.getPhotoId(), ownerId)) + .thenReturn(Image.builder().build()); + + tested.fetchTrustedPhotoFromDocumentVerifier(ownerId, identityVerification); - service.selectPhotoForPresenceCheck(ownerId, documentsReversedOrder); - when(identityVerificationService.getPhotoById(docPhotoIdCard.getPhotoId(), ownerId)).thenReturn(Image.builder().build()); verify(identityVerificationService, times(1)).getPhotoById(docPhotoIdCard.getPhotoId(), ownerId); + } - // Unknown document with a person photo - DocumentVerificationEntity docPhotoUnknown = new DocumentVerificationEntity(); + @Test + void testFetchTrustedPhotoFromDocumentVerifier_unknownDocument() throws Exception { + final OwnerId ownerId = new OwnerId(); + final IdentityVerificationEntity identityVerification = new IdentityVerificationEntity(); + + final DocumentVerificationEntity docPhotoUnknown = new DocumentVerificationEntity(); docPhotoUnknown.setPhotoId("unknownPhotoId"); docPhotoUnknown.setType(DocumentType.UNKNOWN); - List documentUnknown = List.of(docPhotoUnknown); + when(documentVerificationRepository.findAllWithPhoto(identityVerification)) + .thenReturn(List.of(docPhotoUnknown)); + when(identityVerificationService.getPhotoById(docPhotoUnknown.getPhotoId(), ownerId)) + .thenReturn(Image.builder().build()); + + tested.fetchTrustedPhotoFromDocumentVerifier(ownerId, identityVerification); - service.selectPhotoForPresenceCheck(ownerId, documentUnknown); - when(identityVerificationService.getPhotoById(docPhotoUnknown.getPhotoId(), ownerId)).thenReturn(Image.builder().build()); verify(identityVerificationService, times(1)).getPhotoById(docPhotoUnknown.getPhotoId(), ownerId); } diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProviderTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProviderTest.java index 6f3f19242..5361e51d0 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProviderTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProviderTest.java @@ -86,7 +86,7 @@ void getResultTest() { @Test void cleanupIdentityDataTest() { - provider.cleanupIdentityData(ownerId); + provider.cleanupIdentityData(ownerId, new SessionInfo()); } private OwnerId createOwnerId() { From 11bc4fb8223fa61350ff5b44333a029d9241b038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20=C5=A0trobl?= Date: Wed, 6 Dec 2023 14:45:55 +0100 Subject: [PATCH 29/52] Fix issues found during operation claim integration into mobile token (#943) --- docs/Mobile-Token-API.md | 3 +-- .../app/enrollmentserver/impl/service/MobileTokenService.java | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Mobile-Token-API.md b/docs/Mobile-Token-API.md index 2647df518..d155655e3 100644 --- a/docs/Mobile-Token-API.md +++ b/docs/Mobile-Token-API.md @@ -395,8 +395,7 @@ Claim an operation for a user. ```json { "requestObject": { - "id": "7e0ba60f-bf22-4ff5-b999-2733784e5eaa", - "userId": "user12345" + "id": "7e0ba60f-bf22-4ff5-b999-2733784e5eaa" } } ``` diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java index b2f87be5a..5f27018c6 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java @@ -278,7 +278,8 @@ public Response operationReject( */ public Operation getOperationDetail(String operationId, String language, String userId) throws MobileTokenException, PowerAuthClientException, MobileTokenConfigurationException { final OperationDetailResponse operationDetail = getOperationDetailInternal(operationId); - if (!userId.equals(operationDetail.getUserId())) { + // Check user ID against authenticated user, however skip the check in case operation is not claimed yet + if (operationDetail.getUserId() != null && !userId.equals(operationDetail.getUserId())) { logger.warn("User ID from operation does not match authenticated user ID."); throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Invalid request"); } From c46e03226f21856c221f278d733579309e9b1241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Thu, 7 Dec 2023 09:40:39 +0100 Subject: [PATCH 30/52] Fix #942: Rename proximity anti-fraud check parameters (#944) * Fix #942: Rename proximity anti-fraud check parameters --- .../controller/api/MobileTokenController.java | 2 +- .../model/request/OperationApproveRequest.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java index 1a901b16f..3da69aba1 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java @@ -300,7 +300,7 @@ private static String fetchProximityCheckOtp(OperationApproveRequest requestObje return null; } final var proximityCheck = requestObject.getProximityCheck().get(); - logger.info("Operation ID: {} using proximity check OTP, timestampRequested: {}, timestampSigned: {}", requestObject.getId(), proximityCheck.getTimestampRequested(), proximityCheck.getTimestampSigned()); + logger.info("Operation ID: {} using proximity check OTP, timestampReceived: {}, timestampSent: {}", requestObject.getId(), proximityCheck.getTimestampReceived(), proximityCheck.getTimestampSent()); return proximityCheck.getOtp(); } diff --git a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java index 00c528aca..4a633f862 100644 --- a/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java +++ b/mtoken-model/src/main/java/com/wultra/security/powerauth/lib/mtoken/model/request/OperationApproveRequest.java @@ -60,16 +60,16 @@ public static class ProximityCheck { private Type type; /** - * When OTP obtained by the client. An optional hint for possible better estimation of the time shift correction. + * When OTP received by the client. An optional hint for possible better estimation of the time shift correction. */ - @Schema(description = "When OTP requested by the client. An optional hint for possible better estimation of the time shift correction.") - private Instant timestampRequested; + @Schema(description = "When OTP received by the client. An optional hint for possible better estimation of the time shift correction.") + private Instant timestampReceived; /** - * When OTP signed by the client. An optional hint for possible better estimation of the time shift correction. + * When OTP is used by the client as part of a signed message. An optional hint for possible better estimation of the time shift correction. */ - @Schema(description = "When OTP signed by the client. An optional hint for possible better estimation of the time shift correction.") - private Instant timestampSigned; + @Schema(description = "When OTP is used by the client as part of a signed message. An optional hint for possible better estimation of the time shift correction.") + private Instant timestampSent; public enum Type { QR_CODE, From 80a1bfe966ae8afba554c6192683cab2d518befa Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Fri, 8 Dec 2023 08:26:03 +0100 Subject: [PATCH 31/52] Fix #945: Coverity: Eq: Problems with implementation of equals() - add equals and hashcode - make ActivationOtpErrorResponse immutable --- .../response/error/ActivationOtpErrorResponse.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/error/ActivationOtpErrorResponse.java b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/error/ActivationOtpErrorResponse.java index 43e1dc5aa..11c8d181c 100644 --- a/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/error/ActivationOtpErrorResponse.java +++ b/enrollment-server-onboarding-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/onboarding/response/error/ActivationOtpErrorResponse.java @@ -19,21 +19,23 @@ import io.getlime.core.rest.model.base.response.ErrorResponse; import jakarta.validation.constraints.NotBlank; +import lombok.EqualsAndHashCode; /** * Response class used when OTP code verification fails during activation (soft fail). * * @author Roman Strobl, roman.strobl@wultra.com */ +@EqualsAndHashCode(callSuper = true) public class ActivationOtpErrorResponse extends ErrorResponse { - private Integer remainingAttempts; + private final Integer remainingAttempts; /** * Default constructor. */ public ActivationOtpErrorResponse() { - super(); + remainingAttempts = null; } /** @@ -55,12 +57,4 @@ public Integer getRemainingAttempts() { return remainingAttempts; } - /** - * Set remaining attempts for OTP verification during activation. - * @param remainingAttempts Remaining attempts for OTP verification during activation. - */ - public void setRemainingAttempts(Integer remainingAttempts) { - this.remainingAttempts = remainingAttempts; - } - } From 8b59b4e28e8bad1fe1be17840db891fabce8c73e Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Fri, 8 Dec 2023 18:25:16 +0100 Subject: [PATCH 32/52] Fix #947: Update logback --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 22b5f3556..41d0c288f 100644 --- a/pom.xml +++ b/pom.xml @@ -106,6 +106,8 @@ 1.77 7.4 + + 1.4.14 From fa9e50a6cc1a8bb1867e4f59722040a7d6471314 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 02:32:37 +0000 Subject: [PATCH 33/52] Bump io.swagger.core.v3:swagger-annotations-jakarta Bumps io.swagger.core.v3:swagger-annotations-jakarta from 2.2.15 to 2.2.19. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-annotations-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 22b5f3556..dab0ff48c 100644 --- a/pom.xml +++ b/pom.xml @@ -92,7 +92,7 @@ 5.10.0 3.2.1 - 2.2.15 + 2.2.19 2.2.0 1.4.2 From afd550cba01f8f3ad49bf6220a1dcbbba7626a88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 02:33:20 +0000 Subject: [PATCH 34/52] Bump net.javacrumbs.shedlock:shedlock-bom from 5.10.0 to 5.10.2 Bumps [net.javacrumbs.shedlock:shedlock-bom](https://github.com/lukas-krecan/ShedLock) from 5.10.0 to 5.10.2. - [Commits](https://github.com/lukas-krecan/ShedLock/compare/shedlock-parent-5.10.0...shedlock-parent-5.10.2) --- updated-dependencies: - dependency-name: net.javacrumbs.shedlock:shedlock-bom dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 22b5f3556..8cc848da4 100644 --- a/pom.xml +++ b/pom.xml @@ -90,7 +90,7 @@ 7.0.0 - 5.10.0 + 5.10.2 3.2.1 2.2.15 2.2.0 From e97e454f5970e7889948d46b701304411652f87e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 02:33:24 +0000 Subject: [PATCH 35/52] Bump org.openapitools:openapi-generator-maven-plugin from 7.0.0 to 7.1.0 Bumps org.openapitools:openapi-generator-maven-plugin from 7.0.0 to 7.1.0. --- updated-dependencies: - dependency-name: org.openapitools:openapi-generator-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 22b5f3556..02c57c955 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ - 7.0.0 + 7.1.0 5.10.0 3.2.1 From af14b76cbbe0fa2b586f8209b862dc1342f86dc1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 05:44:21 +0000 Subject: [PATCH 36/52] Bump org.springdoc:springdoc-openapi-starter-webmvc-ui Bumps [org.springdoc:springdoc-openapi-starter-webmvc-ui](https://github.com/springdoc/springdoc-openapi) from 2.2.0 to 2.3.0. - [Release notes](https://github.com/springdoc/springdoc-openapi/releases) - [Changelog](https://github.com/springdoc/springdoc-openapi/blob/main/CHANGELOG.md) - [Commits](https://github.com/springdoc/springdoc-openapi/compare/v2.2.0...v2.3.0) --- updated-dependencies: - dependency-name: org.springdoc:springdoc-openapi-starter-webmvc-ui dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fd97bd882..5352dd501 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ 5.10.2 3.2.1 2.2.19 - 2.2.0 + 2.3.0 1.4.2 From 74edc3a826d9ffa540b0221bec5ed9668e7c2b39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 05:44:21 +0000 Subject: [PATCH 37/52] Bump spring-statemachine.version from 3.2.1 to 4.0.0 Bumps `spring-statemachine.version` from 3.2.1 to 4.0.0. Updates `org.springframework.statemachine:spring-statemachine-starter` from 3.2.1 to 4.0.0 - [Release notes](https://github.com/spring-projects/spring-statemachine/releases) - [Commits](https://github.com/spring-projects/spring-statemachine/compare/v3.2.1...v4.0.0) Updates `org.springframework.statemachine:spring-statemachine-test` from 3.2.1 to 4.0.0 - [Release notes](https://github.com/spring-projects/spring-statemachine/releases) - [Commits](https://github.com/spring-projects/spring-statemachine/compare/v3.2.1...v4.0.0) --- updated-dependencies: - dependency-name: org.springframework.statemachine:spring-statemachine-starter dependency-type: direct:production update-type: version-update:semver-major - dependency-name: org.springframework.statemachine:spring-statemachine-test dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fd97bd882..9a87d1ec4 100644 --- a/pom.xml +++ b/pom.xml @@ -91,7 +91,7 @@ 7.1.0 5.10.2 - 3.2.1 + 4.0.0 2.2.19 2.2.0 1.4.2 From ad8bf8985cc45febf2dea01966ee399ece10feb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Tue, 12 Dec 2023 15:48:14 +0100 Subject: [PATCH 38/52] Fix #911: Expose API to upload presence check data (#941) * Fix #911: Expose API to upload presence check data --- .../IdentityVerificationException.java | 4 + .../innovatrics/InnovatricsApiService.java | 67 ++++++++- .../InnovatricsLivenessController.java | 93 ++++++++++++ .../InnovatricsLivenessService.java | 134 ++++++++++++++++++ .../configuration/OpenApiConfiguration.java | 3 +- 5 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessController.java create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/errorhandling/IdentityVerificationException.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/errorhandling/IdentityVerificationException.java index 401f15436..f403b7bcc 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/errorhandling/IdentityVerificationException.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/errorhandling/IdentityVerificationException.java @@ -36,4 +36,8 @@ public IdentityVerificationException(String message) { super(message); } + public IdentityVerificationException(String message, Throwable cause) { + super(message, cause); + } + } \ No newline at end of file diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java index f4a6a2089..8d99e710e 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java @@ -19,9 +19,7 @@ import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CustomerInspectResponse; -import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessRequest; -import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*; import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientException; import lombok.extern.slf4j.Slf4j; @@ -29,6 +27,8 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; @@ -145,4 +145,65 @@ public void deleteSelfie(final String customerId, final OwnerId ownerId) throws } } + public void createLiveness(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/liveness".formatted(customerId); + + try { + logger.info("Calling liveness creation, {}", ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.put(apiPath, null, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for liveness creation, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + } catch (RestClientException e) { + throw new RemoteCommunicationException( + String.format("Failed REST call to liveness creation for customerId=%s, statusCode=%s, responseBody='%s'", customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when creating liveness for customerId=" + customerId, e); + } + } + + public CreateCustomerLivenessRecordResponse createLivenessRecord(final String customerId, final byte[] requestData, final OwnerId ownerId) throws RemoteCommunicationException{ + final String apiPath = "/api/v1/customers/%s/liveness/records".formatted(customerId); + + final HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM); + + try { + logger.info("Calling liveness record creation, {}", ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.post(apiPath, requestData, EMPTY_QUERY_PARAMS, httpHeaders, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for liveness record creation, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException( + String.format("Failed REST call to liveness record creation for customerId=%s, statusCode=%s, responseBody='%s'", customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when creating liveness record for customerId=" + customerId, e); + } + } + + public CreateSelfieResponse createSelfie(final String customerId, final String livenessSelfieLink, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/selfie".formatted(customerId); + + final CreateSelfieRequest request = new CreateSelfieRequest().selfieOrigin(new LivenessSelfieOrigin().link(livenessSelfieLink)); + + try { + logger.info("Calling selfie creation, {}", ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.put(apiPath, request, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for selfie creation, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException( + String.format("Failed REST call to selfie creation for customerId=%s, statusCode=%s, responseBody='%s'", customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when creating selfie for customerId=" + customerId, e); + } + } + } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessController.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessController.java new file mode 100644 index 000000000..cdc3ab460 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessController.java @@ -0,0 +1,93 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; +import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import io.getlime.core.rest.model.base.response.Response; +import io.getlime.security.powerauth.crypto.lib.enums.PowerAuthSignatureTypes; +import io.getlime.security.powerauth.rest.api.spring.annotation.EncryptedRequestBody; +import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuth; +import io.getlime.security.powerauth.rest.api.spring.annotation.PowerAuthEncryption; +import io.getlime.security.powerauth.rest.api.spring.authentication.PowerAuthApiAuthentication; +import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionContext; +import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionScope; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthAuthenticationException; +import io.getlime.security.powerauth.rest.api.spring.exception.PowerAuthEncryptionException; +import io.getlime.security.powerauth.rest.api.spring.exception.authentication.PowerAuthTokenInvalidException; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +/** + * Controller publishing REST services for uploading Innovatrics liveness data. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ConditionalOnExpression(""" + '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and ${enrollment-server-onboarding.onboarding-process.enabled} == true + """) +@RestController +@RequestMapping(value = "api/identity") +@AllArgsConstructor +@Slf4j +class InnovatricsLivenessController { + + private InnovatricsLivenessService innovatricsLivenessService; + + /** + * Upload Innovatrics liveness data. + * + * @param requestData Binary request data + * @param encryptionContext Encryption context. + * @param apiAuthentication PowerAuth authentication. + * @return Presence check initialization response. + * @throws PowerAuthAuthenticationException Thrown when request authentication fails. + * @throws PowerAuthEncryptionException Thrown when request decryption fails. + * @throws IdentityVerificationException Thrown when identity verification is invalid. + * @throws RemoteCommunicationException Thrown when there is a problem with the remote communication. + */ + @PostMapping("presence-check/upload") + @PowerAuthEncryption(scope = EncryptionScope.ACTIVATION_SCOPE) + @PowerAuth(resourceId = "/api/identity/presence-check/upload", signatureType = PowerAuthSignatureTypes.POSSESSION) + public Response upload( + @EncryptedRequestBody byte[] requestData, + @Parameter(hidden = true) EncryptionContext encryptionContext, + @Parameter(hidden = true) PowerAuthApiAuthentication apiAuthentication) throws IdentityVerificationException, PowerAuthAuthenticationException, PowerAuthEncryptionException, RemoteCommunicationException { + + if (apiAuthentication == null) { + throw new PowerAuthTokenInvalidException("Unable to verify device registration when uploading liveness"); + } + + if (encryptionContext == null) { + throw new PowerAuthEncryptionException("ECIES encryption failed when uploading liveness"); + } + + if (requestData == null) { + throw new PowerAuthEncryptionException("Invalid request received when uploading liveness"); + } + + innovatricsLivenessService.upload(requestData, encryptionContext); + return new Response(); + } +} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java new file mode 100644 index 000000000..833396add --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java @@ -0,0 +1,134 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.enrollmentserver.model.integration.SessionInfo; +import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository; +import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; +import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; +import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import com.wultra.app.onboardingserver.common.service.AuditService; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateCustomerLivenessRecordResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateSelfieResponse; +import io.getlime.security.powerauth.rest.api.spring.encryption.EncryptionContext; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service providing Innovatrics business features beyond {@link InnovatricsPresenceCheckProvider}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Service +@Transactional(readOnly = true) +@Slf4j +@AllArgsConstructor +@ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "innovatrics") +class InnovatricsLivenessService { + + private static final String INNOVATRICS_CUSTOMER_ID = "InnovatricsCustomerId"; + + private final InnovatricsApiService innovatricsApiService; + + private final IdentityVerificationRepository identityVerificationRepository; + + private AuditService auditService; + + public void upload(final byte[] requestData, final EncryptionContext encryptionContext) throws IdentityVerificationException, RemoteCommunicationException { + final String activationId = encryptionContext.getActivationId(); + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findFirstByActivationIdOrderByTimestampCreatedDesc(activationId).orElseThrow(() -> + new IdentityVerificationException("No identity verification entity found for Activation ID: " + activationId)); + + final OwnerId ownerId = extractOwnerId(identityVerification); + final String customerId = fetchCustomerId(ownerId, identityVerification); + + createLiveness(customerId, ownerId); + final CreateCustomerLivenessRecordResponse livenessRecordResponse = createLivenessRecord(requestData, customerId, ownerId); + createSelfie(livenessRecordResponse, customerId, ownerId); + + auditService.auditPresenceCheckProvider(identityVerification, "Uploaded presence check data for user: {}", ownerId.getUserId()); + logger.info("Liveness record successfully uploaded, {}", ownerId); + } + + private void createSelfie(final CreateCustomerLivenessRecordResponse livenessRecordResponse, final String customerId, final OwnerId ownerId) throws IdentityVerificationException, RemoteCommunicationException { + final String livenessSelfieLink = fetchSelfieLink(livenessRecordResponse); + final CreateSelfieResponse createSelfieResponse = innovatricsApiService.createSelfie(customerId, livenessSelfieLink, ownerId); + if (createSelfieResponse.getErrorCode() != null) { + logger.warn("Customer selfie error: {}, {}", createSelfieResponse.getErrorCode(), ownerId); + } + if (createSelfieResponse.getWarnings() != null) { + for (CreateSelfieResponse.WarningsEnum warning : createSelfieResponse.getWarnings()) { + logger.warn("Customer selfie warning: {}, {}", warning.getValue(), ownerId); + } + } + logger.debug("Selfie created, {}", ownerId); + } + + private CreateCustomerLivenessRecordResponse createLivenessRecord(final byte[] requestData, final String customerId, final OwnerId ownerId) throws RemoteCommunicationException, IdentityVerificationException { + final CreateCustomerLivenessRecordResponse livenessRecordResponse = innovatricsApiService.createLivenessRecord(customerId, requestData, ownerId); + if (livenessRecordResponse.getErrorCode() != null) { + throw new IdentityVerificationException("Unable to create liveness record: " + livenessRecordResponse.getErrorCode()); + } + logger.debug("Liveness record created, {}", ownerId); + return livenessRecordResponse; + } + + private void createLiveness(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + innovatricsApiService.createLiveness(customerId, ownerId); + logger.debug("Liveness created, {}", ownerId); + } + + private static String fetchSelfieLink(final CreateCustomerLivenessRecordResponse livenessRecordResponse) throws IdentityVerificationException { + if (livenessRecordResponse.getLinks() == null) { + throw new IdentityVerificationException("Unable to get selfie link"); + } + return livenessRecordResponse.getLinks().getSelfie(); + } + + private static OwnerId extractOwnerId(final IdentityVerificationEntity identityVerification) { + final OwnerId ownerId = new OwnerId(); + ownerId.setActivationId(identityVerification.getActivationId()); + ownerId.setUserId(identityVerification.getUserId()); + return ownerId; + } + + private static String fetchCustomerId(final OwnerId id, final IdentityVerificationEntity identityVerification) throws IdentityVerificationException { + final String sessionInfoString = StringUtils.defaultIfEmpty(identityVerification.getSessionInfo(), "{}"); + final SessionInfo sessionInfo; + try { + sessionInfo = new ObjectMapper().readValue(sessionInfoString, SessionInfo.class); + } catch (JsonProcessingException e) { + throw new IdentityVerificationException("Unable to deserialize session info", e); + } + + // TODO (racansky, 2023-11-28) discuss the format with Jan Pesek, extract to common logic + final String customerId = (String) sessionInfo.getSessionAttributes().get(INNOVATRICS_CUSTOMER_ID); + if (Strings.isNullOrEmpty(customerId)) { + throw new IdentityVerificationException("Missing a customer ID value for calling Innovatrics, " + id); + } + return customerId; + } +} diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/OpenApiConfiguration.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/OpenApiConfiguration.java index ef8410989..4a4dce8d2 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/OpenApiConfiguration.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/OpenApiConfiguration.java @@ -53,7 +53,8 @@ public class OpenApiConfiguration { public GroupedOpenApi defaultApiGroup() { String[] packages = { "io.getlime.security.powerauth", - "com.wultra.app.onboardingserver.controller.api" + "com.wultra.app.onboardingserver.controller.api", + "com.wultra.app.onboardingserver.provider.innovatrics" }; return GroupedOpenApi.builder() From ed971a4628ac174fc8941817ff96b78ae90ad03a Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Wed, 13 Dec 2023 09:16:57 +0100 Subject: [PATCH 39/52] Fix #954: Update Wultra dependencies --- pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 6cd680b3d..535e57391 100644 --- a/pom.xml +++ b/pom.xml @@ -99,10 +99,10 @@ 3.13.0 - 1.8.0-SNAPSHOT - 1.6.0-SNAPSHOT - 1.6.0-SNAPSHOT - 1.6.0-SNAPSHOT + 1.8.0 + 1.6.0 + 1.6.0 + 1.6.0 1.77 7.4 From 34a30ab46bc3b1956e92a47de5cba8b37987b48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pe=C5=A1ek?= Date: Thu, 14 Dec 2023 11:10:26 +0100 Subject: [PATCH 40/52] Fix #907: Innovatrics DocumentVerificationProvider (#938) * Fix #907: Innovatrics DocumentVerificationProvider * Merge providers * Do not check consistency of each attribute of a document --- docs/onboarding/Configuration-Properties.md | 28 +- .../Configuration-Verification-Providers.md | 2 + .../api/provider/PresenceCheckProvider.java | 20 +- .../model/integration/SessionInfo.java | 5 + .../pom.xml | 12 + .../innovatrics/InnovatricsApiService.java | 277 ++++++++++++- .../innovatrics/InnovatricsConfigProps.java | 18 + ...novatricsDocumentVerificationProvider.java | 363 +++++++++++++++++- .../InnovatricsLivenessService.java | 11 +- .../InnovatricsPresenceCheckProvider.java | 16 +- ...tricsDocumentVerificationProviderTest.java | 174 +++++++++ .../InnovatricsPresenceCheckProviderTest.java | 2 +- .../InnovatricsRestApiServiceTest.java | 138 +++++++ .../resources/application-test.properties | 8 + .../iproov/IProovPresenceCheckProvider.java | 4 +- ...ultraMockDocumentVerificationProvider.java | 6 +- .../impl/service/PresenceCheckService.java | 62 ++- .../document/DocumentProcessingService.java | 219 ++++++++--- .../WultraMockPresenceCheckProvider.java | 4 +- .../src/main/resources/application.properties | 6 +- ...aMockDocumentVerificationProviderTest.java | 2 +- .../service/PresenceCheckServiceTest.java | 74 +++- .../DocumentProcessingServiceTest.java | 282 ++++++++++++-- .../DocumentProcessingServiceTest.sql | 2 + ...gServiceTest.testPairTwoSidedDocuments.sql | 6 + ...ssingServiceTest.testResubmitDocuments.sql | 6 + 26 files changed, 1568 insertions(+), 179 deletions(-) create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProviderTest.java create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsRestApiServiceTest.java create mode 100644 enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.sql create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testPairTwoSidedDocuments.sql create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testResubmitDocuments.sql diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index cbd0952d0..1918751fb 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -137,19 +137,21 @@ The Onboarding Server uses the following public configuration properties: ## Innovatrics Configuration -| Property | Default | Note | -|--------------------------------------------------------------------------------------------------|---------------------------|--------------------------------------------------------------------------------| -| `enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl` | | Base REST service URL for Innovatrics. | -| `enrollment-server-onboarding.provider.innovatrics.serviceToken` | | Authentication token for Innovatrics. | -| `enrollment-server-onboarding.provider.innovatrics.serviceUserAgent` | `Wultra/OnboardingServer` | User agent to use when making HTTP calls to Innovatrics REST service. | -| `enrollment-server-onboarding.provider.innovatrics.presenceCheck.score` | 0.875 | Presence check minimal score threshold. | -| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate` | `false` | Whether invalid SSL certificate is accepted when calling Zen ID REST service. | -| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling Innovatrics REST service. | -| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling Innovatrics REST service. | -| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyHost` | | Proxy host to be used when calling Innovatrics REST service. | -| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPort` | 0 | Proxy port to be used when calling Innovatrics REST service. | -| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyUsername` | | Proxy username to be used when calling Innovatrics REST service. | -| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPassword` | | Proxy password to be used when calling Innovatrics REST service. | +| Property | Default | Note | +|---------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl` | | Base REST service URL for Innovatrics. | +| `enrollment-server-onboarding.provider.innovatrics.serviceToken` | | Authentication token for Innovatrics. | +| `enrollment-server-onboarding.provider.innovatrics.serviceUserAgent` | `Wultra/OnboardingServer` | User agent to use when making HTTP calls to Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.presenceCheckConfiguration.score` | 0.875 | Presence check minimal score threshold. | +| `enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries` | `CZE` | List of expected countries of issue of identification documents as three-letter country codes, i.e. ISO 3166-1 alpha-3. If empty, all countries of issue known to Innovatrics are considered during classification, which may have negative impact on performance. | +| `enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields` | `documentNumber`, `dateOfIssue`, `dateOfExpiry`, `surname`, `dateOfBirth`, `personalNumber`, `givenNames` | Set of fields in camelCase that are cross-validated between the machine-readable zone and visual zone. Only those specified fields that are actually extracted from a document are considered. If empty, no inconsistencies will be checked. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate` | `false` | Whether invalid SSL certificate is accepted when calling Zen ID REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyHost` | | Proxy host to be used when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPort` | 0 | Proxy port to be used when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyUsername` | | Proxy username to be used when calling Innovatrics REST service. | +| `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyPassword` | | Proxy password to be used when calling Innovatrics REST service. | See [Innovatrics documentation](https://developers.innovatrics.com/digital-onboarding/docs/functionalities/face/active-liveness-check/#magnifeye-liveness) for details how the score affects false acceptances (FAR) and false rejections (FRR). diff --git a/docs/onboarding/Configuration-Verification-Providers.md b/docs/onboarding/Configuration-Verification-Providers.md index ea4f83544..ed358b8bd 100644 --- a/docs/onboarding/Configuration-Verification-Providers.md +++ b/docs/onboarding/Configuration-Verification-Providers.md @@ -6,6 +6,7 @@ This document describes configuration of providers for personal identity documen The document verification process is currently supported for following providers: - [ZenID](https://zenid.trask.cz/) - use value `zenid` in configuration +- [Innovatrics](https://www.innovatrics.com/) - use value `innovatrics` in configuration - Mock - useful for simple testing and local runs - use value `mock` in configuration ### ZenID @@ -35,6 +36,7 @@ When calling `document-verification/init-sdk` following implementation fields ar The document verification process is currently supported for following providers: - [iProov](https://www.iproov.com/) - use value `iproov` in configuration +- [Innovatrics](https://www.innovatrics.com/) - use value `innovatrics` in configuration - Mock - useful for simple testing and local runs - use value `mock` in configuration #### Configuration diff --git a/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java index 298b5296b..2a4ace4ac 100644 --- a/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java +++ b/enrollment-server-onboarding-api/src/main/java/com/wultra/app/onboardingserver/api/provider/PresenceCheckProvider.java @@ -44,13 +44,13 @@ public interface PresenceCheckProvider { void initPresenceCheck(OwnerId id, Image photo) throws PresenceCheckException, RemoteCommunicationException; /** - * A feature flag whether the trusted photo of the user should be passed to {@link #initPresenceCheck(OwnerId, Image)}. + * Configuration flag setting where the provider implementation expects the trusted photo of the user. *

    * Some implementation may require specific source to be called by Onboarding server, some providers may handle it internally. * - * @return {@code true} if the trusted photo should be provided, {@code false} otherwise. + * @return Source where the trusted photo is expected. */ - boolean shouldProvideTrustedPhoto(); + TrustedPhotoSource trustedPhotoSource(); /** * Starts the presence check process. The process has to be initialized before this call. @@ -82,4 +82,18 @@ public interface PresenceCheckProvider { */ void cleanupIdentityData(OwnerId id, SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException; + /** + * Return type for {@link #trustedPhotoSource()}. + */ + enum TrustedPhotoSource { + /** + * If the trusted photo should be passed in {@link #initPresenceCheck} + */ + IMAGE, + + /** + * If the trusted photo is passed via reference in {@link SessionInfo} + */ + REFERENCE + } } diff --git a/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/SessionInfo.java b/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/SessionInfo.java index 1dfec196f..89aed8802 100644 --- a/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/SessionInfo.java +++ b/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/SessionInfo.java @@ -30,6 +30,11 @@ @Data public class SessionInfo { + public static final String ATTRIBUTE_TIMESTAMP_LAST_USED = "timestampLastUsed"; + public static final String ATTRIBUTE_IMAGE_UPLOADED = "imageUploaded"; + public static final String ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE = "primaryDocumentReference"; + public static final String ATTRIBUTE_OTHER_DOCUMENTS_REFERENCES = "otherDocumentsReferences"; + private Map sessionAttributes = new LinkedHashMap<>(); } diff --git a/enrollment-server-onboarding-provider-innovatrics/pom.xml b/enrollment-server-onboarding-provider-innovatrics/pom.xml index 73dd6c2b0..bec000cb1 100644 --- a/enrollment-server-onboarding-provider-innovatrics/pom.xml +++ b/enrollment-server-onboarding-provider-innovatrics/pom.xml @@ -42,6 +42,18 @@ spring-boot-starter-test test + + + com.squareup.okhttp3 + mockwebserver + test + + + + com.h2database + h2 + test + diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java index 8d99e710e..916c08fb7 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java @@ -18,7 +18,13 @@ package com.wultra.app.onboardingserver.provider.innovatrics; import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CustomerInspectResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessRequest; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.EvaluateCustomerLivenessResponse; +import com.wultra.app.enrollmentserver.model.enumeration.CardSide; +import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*; import com.wultra.core.rest.client.base.RestClient; import com.wultra.core.rest.client.base.RestClientException; @@ -29,11 +35,15 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import java.util.List; +import java.util.Optional; + /** * Implementation of the REST service toInnovatrics. *

    @@ -41,6 +51,7 @@ * Both providers, document verifier and presence check, must be configured to {@code innovatrics}. * * @author Lubos Racansky, lubos.racansky@wultra.com + * @author Jan Pesek, jan.pesek@wultra.com */ @ConditionalOnExpression(""" '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and '${enrollment-server-onboarding.document-verification.provider}' == 'innovatrics' @@ -49,8 +60,6 @@ @Slf4j class InnovatricsApiService { - private static final MultiValueMap EMPTY_ADDITIONAL_HEADERS = new LinkedMultiValueMap<>(); - private static final MultiValueMap EMPTY_QUERY_PARAMS = new LinkedMultiValueMap<>(); /** @@ -58,14 +67,21 @@ class InnovatricsApiService { */ private final RestClient restClient; + /** + * Configuration properties. + */ + private final InnovatricsConfigProps configProps; + /** * Service constructor. * * @param restClient REST template for Innovatrics calls. */ @Autowired - public InnovatricsApiService(@Qualifier("restClientInnovatrics") final RestClient restClient) { + public InnovatricsApiService(@Qualifier("restClientInnovatrics") final RestClient restClient, + InnovatricsConfigProps configProps) { this.restClient = restClient; + this.configProps = configProps; } public EvaluateCustomerLivenessResponse evaluateLiveness(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { @@ -77,7 +93,7 @@ public EvaluateCustomerLivenessResponse evaluateLiveness(final String customerId try { logger.info("Calling liveness/evaluation, {}", ownerId); logger.debug("Calling {}, {}", apiPath, request); - final ResponseEntity response = restClient.post(apiPath, request, EMPTY_QUERY_PARAMS, EMPTY_ADDITIONAL_HEADERS, new ParameterizedTypeReference<>() {}); + final ResponseEntity response = restClient.post(apiPath, request, new ParameterizedTypeReference<>() {}); logger.info("Got {} for liveness/evaluation, {}", response.getStatusCode(), ownerId); logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); logger.trace("{} response: {}", apiPath, response); @@ -96,7 +112,7 @@ public CustomerInspectResponse inspectCustomer(final String customerId, final Ow try { logger.info("Calling /inspect, {}", ownerId); logger.debug("Calling {}", apiPath); - final ResponseEntity response = restClient.post(apiPath, null, EMPTY_QUERY_PARAMS, EMPTY_ADDITIONAL_HEADERS, new ParameterizedTypeReference<>() {}); + final ResponseEntity response = restClient.post(apiPath, null, new ParameterizedTypeReference<>() {}); logger.info("Got {} for /inspect, {}", response.getStatusCode(), ownerId); logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); logger.trace("{} response: {}", apiPath, response); @@ -206,4 +222,255 @@ public CreateSelfieResponse createSelfie(final String customerId, final String l } } + // TODO remove - temporal test call +// @PostConstruct +// public void testCall() throws RestClientException { +// logger.info("Trying a test call"); +// final ResponseEntity response = restClient.get("/api/v1/metadata", STRING_TYPE_REFERENCE); +// logger.info("Result of test call: {}", response.getBody()); +// } + + /** + * Create a new customer resource. + * @param ownerId owner identification. + * @return optional of CreateCustomerResponse with a customerId. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public CreateCustomerResponse createCustomer(final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers"; + + try { + logger.info("Creating customer, {}", ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.post(apiPath, null, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for creating customer, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException("REST API call failed when creating a new customer resource, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when creating a new customer resource", e); + } + } + + /** + * Create a new document resource assigned to a customer. This resource is used for documents only, not for selfies. + * @param customerId id of the customer to assign the resource to. + * @param documentType type of document that will be uploaded later. + * @param ownerId owner identification. + * @return optional of CreateDocumentResponse. Does not contain important details. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public CreateDocumentResponse createDocument(final String customerId, final DocumentType documentType, final OwnerId ownerId) throws RemoteCommunicationException, DocumentVerificationException { + final String apiPath = "/api/v1/customers/%s/document".formatted(customerId); + + final DocumentClassificationAdvice classificationAdvice = new DocumentClassificationAdvice(); + classificationAdvice.setTypes(List.of(convertType(documentType))); + classificationAdvice.setCountries(configProps.getDocumentVerificationConfiguration().getDocumentCountries()); + final DocumentAdvice advice = new DocumentAdvice(); + advice.setClassification(classificationAdvice); + final CreateDocumentRequest request = new CreateDocumentRequest(); + request.setAdvice(advice); + + try { + logger.info("Creating new document of type {} for customer {}, {}", documentType, customerId, ownerId); + logger.debug("Calling {}, {}", apiPath, request); + final ResponseEntity response = restClient.put(apiPath, request, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for creating document, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException("REST API call failed when creating a new document resource for customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when creating a new document resource for customerId=%s".formatted(customerId), e); + } + } + + /** + * Provide photo of a document page. A document resource must be already assigned to the customer. + * @param customerId id of the customer to whom the document should be provided. + * @param side specifies side of the document. + * @param imageBytes image of the page encoded in base64. + * @param ownerId owner identification. + * @return optional of CreateDocumentPageResponse with details extracted from the page. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public CreateDocumentPageResponse provideDocumentPage(final String customerId, final CardSide side, final byte[] imageBytes, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/document/pages".formatted(customerId); + + final DocumentPageClassificationAdvice classificationAdvice = new DocumentPageClassificationAdvice(); + classificationAdvice.setPageTypes(List.of(convertSide(side))); + final DocumentPageAdvice advice = new DocumentPageAdvice(); + advice.setClassification(classificationAdvice); + + final Image image = new Image(); + image.setData(imageBytes); + + final CreateDocumentPageRequest request = new CreateDocumentPageRequest(); + request.setAdvice(advice); + request.setImage(image); + + try { + logger.info("Providing {} side document page for customer {}, {}", convertSide(side), customerId, ownerId); + logger.debug("Calling {}, {}", apiPath, request); + final ResponseEntity response = restClient.put(apiPath, request, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for providing document page, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException("REST API call failed when providing a document page for customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when providing a document page for customerId=%s".formatted(customerId), e); + } + } + + /** + * Get details gathered about the customer. + * @param customerId id of the customer. + * @param ownerId owner identification. + * @return optional of GetCustomerResponse with details about the customer. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public GetCustomerResponse getCustomer(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s".formatted(customerId); + + try { + logger.info("Getting details about customer {}, {}", customerId, ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.get(apiPath, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for getting details about customer, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException("REST API call failed when getting details of customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when getting details of customerId=%s".formatted(customerId), e); + } + } + + /** + * Get document portrait of the customer. + * @param customerId id of the customer. + * @param ownerId owner identification. + * @return successful Response contains a base64 in the JPG format. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public Optional getDocumentPortrait(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/document/portrait".formatted(customerId); + + try { + logger.info("Getting document portrait of customer {}, {}", customerId, ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.get(apiPath, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for getting document portrait, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return Optional.ofNullable(response.getBody()); + } catch (RestClientException e) { + if (HttpStatus.NOT_FOUND == e.getStatusCode()) { + // API returns 404 in case of missing portrait photo. + logger.debug("Missing portrait photo for customer {}, {}", customerId, ownerId); + return Optional.empty(); + } + throw new RemoteCommunicationException("REST API call failed when getting customer portrait, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e); + } + } + + /** + * Inspect consistency of data of the submitted document provided for a customer. + * @param customerId id of the customer whose document to inspect. + * @param ownerId owner identification. + * @return optional of DocumentInspectResponse with details about consistency of the document. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public DocumentInspectResponse inspectDocument(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/document/inspect".formatted(customerId); + + try { + logger.info("Getting document inspect of customer {}, {}", customerId, ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.post(apiPath, null, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for getting document inspect, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + return response.getBody(); + } catch (RestClientException e) { + throw new RemoteCommunicationException("REST API call failed while getting document inspection for customerId=%s, statusCode=%s, responseBody='%s'".formatted(customerId, e.getStatusCode(), e.getResponse()), e); + } catch (Exception e) { + throw new RemoteCommunicationException("Unexpected error when getting document inspection for customerId=%s".formatted(customerId), e); + } + } + + /** + * Delete customer. + * @param customerId id of the customer. + * @param ownerId owner identification. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public void deleteCustomer(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s".formatted(customerId); + + try { + logger.info("Deleting customer {}, {}", customerId, ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.delete(apiPath, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for deleting customer, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + } catch (RestClientException e) { + throw new RemoteCommunicationException("REST API call failed when deleting customer, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e); + } + } + + /** + * Delete customer's document. + * @param customerId id of the customer. + * @param ownerId owner identification. + * @throws RemoteCommunicationException in case of 4xx or 5xx response status code. + */ + public void deleteDocument(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + final String apiPath = "/api/v1/customers/%s/document".formatted(customerId); + + try { + logger.info("Deleting document of customer {}, {}", customerId, ownerId); + logger.debug("Calling {}", apiPath); + final ResponseEntity response = restClient.delete(apiPath, new ParameterizedTypeReference<>() {}); + logger.info("Got {} for deleting customer's document, {}", response.getStatusCode(), ownerId); + logger.debug("{} response status code: {}", apiPath, response.getStatusCode()); + logger.trace("{} response: {}", apiPath, response); + } catch (RestClientException e) { + throw new RemoteCommunicationException("REST API call failed when deleting customer's document, statusCode=%s, responseBody='%s'".formatted(e.getStatusCode(), e.getResponse()), e); + } + } + + /** + * Converts internal DocumentType enum to string value used by Innovatrics. + * @param type represents type of document. + * @return document type as a string value. + */ + private static String convertType(DocumentType type) throws DocumentVerificationException { + return switch (type) { + case ID_CARD -> "identity-card"; + case PASSPORT -> "passport"; + case DRIVING_LICENSE -> "drivers-licence"; + default -> throw new DocumentVerificationException("Unsupported documentType " + type); + }; + } + + /** + * Converts internal CardSide enum to string value used by Innovatrics. + * @param side represents side of a card. + * @return side of a card as a string value. + */ + private static String convertSide(CardSide side) { + return switch (side) { + case FRONT -> "front"; + case BACK -> "back"; + }; + } + } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java index 62fb7f67c..ce0acde45 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java @@ -24,6 +24,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import java.util.List; +import java.util.Set; + /** * Innovatrics configuration properties. *

    @@ -62,6 +65,8 @@ class InnovatricsConfigProps { private PresenceCheckConfiguration presenceCheckConfiguration; + private DocumentVerificationConfiguration documentVerificationConfiguration; + @Getter @Setter public static class PresenceCheckConfiguration { /** @@ -70,4 +75,17 @@ public static class PresenceCheckConfiguration { private double score = 0.875; } + @Getter @Setter + public static class DocumentVerificationConfiguration { + /** + * Identifies expected document countries of issue in ISO 3166-1 alpha-3 format. + */ + private List documentCountries; + + /** + * Set of fields in camelCase that are cross-validated between the machine-readable zone and visual zone, if extracted. + */ + private Set crucialFields; + } + } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java index 70d177e73..e3d134135 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java @@ -17,35 +17,100 @@ */ package com.wultra.app.onboardingserver.provider.innovatrics; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; +import com.wultra.app.enrollmentserver.model.enumeration.DocumentVerificationStatus; import com.wultra.app.enrollmentserver.model.integration.*; +import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; /** * Implementation of the {@link DocumentVerificationProvider} with Innovatrics. * * @author Jan Pesek, jan.pesek@wultra.com */ -@ConditionalOnProperty(value = "enrollment-server-onboarding.document-verification.provider", havingValue = "innovatrics") +@ConditionalOnExpression(""" + '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and '${enrollment-server-onboarding.document-verification.provider}' == 'innovatrics' + """) @Component -class InnovatricsDocumentVerificationProvider implements DocumentVerificationProvider { +@AllArgsConstructor +@Slf4j +public class InnovatricsDocumentVerificationProvider implements DocumentVerificationProvider { + + private final InnovatricsApiService innovatricsApiService; + private final ObjectMapper objectMapper; + private final InnovatricsConfigProps configuration; @Override public DocumentsSubmitResult checkDocumentUpload(OwnerId id, DocumentVerificationEntity document) throws RemoteCommunicationException, DocumentVerificationException { - return null; + logger.warn("Unexpected state of document {}, {}", document, id); + throw new UnsupportedOperationException("Method checkDocumentUpload is not supported by Innovatrics provider."); } @Override public DocumentsSubmitResult submitDocuments(OwnerId id, List documents) throws RemoteCommunicationException, DocumentVerificationException { - return null; + if (CollectionUtils.isEmpty(documents)) { + logger.info("Empty documents list passed to document provider, {}", id); + return new DocumentsSubmitResult(); + } + + final DocumentType documentType = documents.get(0).getType(); + if (DocumentType.SELFIE_PHOTO == documentType) { + logger.debug("Selfie photo passed as a document, {}", id); + throw new DocumentVerificationException("Selfie photo cannot be submitted as a document"); + } + + if (DocumentType.SELFIE_VIDEO == documentType) { + logger.debug("Selfie video passed as a document, {}", id); + throw new DocumentVerificationException("Selfie video cannot be submitted as a document"); + } + + final String customerId = createCustomer(id); + createDocument(customerId, documentType, id); + logger.debug("Created new customer {}, {}", customerId, id); + + final DocumentsSubmitResult results = new DocumentsSubmitResult(); + for (SubmittedDocument page : documents) { + final CreateDocumentPageResponse createDocumentPageResponse = provideDocumentPage(customerId, page, id); + if (containsError(createDocumentPageResponse)) { + logger.debug("Page upload was not successful, {}", id); + results.getResults().add(createErrorSubmitResult(customerId, createDocumentPageResponse, page)); + } else { + logger.debug("Document page was read successfully by provider, {}", id); + results.getResults().add(createSubmitResult(customerId, page)); + } + } + + final Optional primaryPage = results.getResults().stream() + .filter(result -> Strings.isNullOrEmpty(result.getRejectReason()) && Strings.isNullOrEmpty(result.getErrorDetail())) + .findFirst(); + + if (primaryPage.isPresent()) { + // Only first found successfully submitted page has extracted data, others has empty JSON + primaryPage.get().setExtractedData(getExtractedData(customerId, id)); + if (hasDocumentPortrait(customerId, id)) { + results.setExtractedPhotoId(customerId); + } + } + + return results; } @Override @@ -55,31 +120,303 @@ public boolean shouldStoreSelfie() { @Override public DocumentsVerificationResult verifyDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException { - return null; + final DocumentsVerificationResult results = new DocumentsVerificationResult(); + results.setResults(new ArrayList<>()); + + // Pages of the same document have same uploadId (= customerId), no reason to generate verification for each one. + final List distinctUploadIds = uploadIds.stream().distinct().toList(); + for (String customerId : distinctUploadIds) { + final DocumentVerificationResult result = createVerificationResult(customerId, id); + results.getResults().add(result); + } + + final String rejectReasons = results.getResults().stream() + .map(DocumentVerificationResult::getRejectReason) + .filter(StringUtils::hasText) + .collect(Collectors.joining(";")); + if (StringUtils.hasText(rejectReasons)) { + logger.debug("Some documents were rejected: rejectReasons={}, {}", rejectReasons, id); + results.setStatus(DocumentVerificationStatus.REJECTED); + results.setRejectReason(rejectReasons); + } else { + logger.debug("All documents accepted, {}", id); + results.setStatus(DocumentVerificationStatus.ACCEPTED); + } + results.setVerificationId(UUID.randomUUID().toString()); + return results; } @Override public DocumentsVerificationResult getVerificationResult(OwnerId id, String verificationId) throws RemoteCommunicationException, DocumentVerificationException { - return null; + logger.warn("Unexpected state of documents with verificationId={}, {}", verificationId, id); + throw new UnsupportedOperationException("Method getVerificationResult is not supported by Innovatrics provider."); } @Override public Image getPhoto(String photoId) throws RemoteCommunicationException, DocumentVerificationException { - return null; + logger.warn("Unexpected document portrait query for customerId={}", photoId); + throw new UnsupportedOperationException("Method getPhoto is not implemented by Innovatrics provider."); } @Override public void cleanupDocuments(OwnerId id, List uploadIds) throws RemoteCommunicationException, DocumentVerificationException { - + // Pages of the same document have same uploadId (= customerId), no reason to call delete for each one. + final List distinctUploadIds = uploadIds.stream().distinct().toList(); + logger.info("Invoked cleanupDocuments, {}", id); + for (String customerId : distinctUploadIds) { + innovatricsApiService.deleteCustomer(customerId, id); + } } @Override public List parseRejectionReasons(DocumentResultEntity docResult) throws DocumentVerificationException { - return null; + logger.debug("Parsing rejection reasons of {}", docResult); + final String rejectionReasons = docResult.getRejectReason(); + if (!StringUtils.hasText(rejectionReasons)) { + return Collections.emptyList(); + } + + return deserializeFromString(rejectionReasons); } @Override public VerificationSdkInfo initVerificationSdk(OwnerId id, Map initAttributes) throws RemoteCommunicationException, DocumentVerificationException { - return null; + logger.debug("#initVerificationSdk does nothing for Innovatrics, {}", id); + return new VerificationSdkInfo(); + } + + /** + * Create a new customer resource. + * @param ownerId owner identification. + * @return ID of the new customer. + * @throws RemoteCommunicationException if the resource was not created properly. + */ + private String createCustomer(final OwnerId ownerId) throws RemoteCommunicationException { + return innovatricsApiService.createCustomer(ownerId).getId(); + } + + /** + * Create a new document resource to an existing customer. + * @param customerId id of the customer to assign the resource to. + * @param documentType type of the document that will be uploaded later. + * @param ownerId owner identification. + * @throws RemoteCommunicationException if the resource was not created properly. + */ + private void createDocument(final String customerId, final DocumentType documentType, final OwnerId ownerId) throws RemoteCommunicationException, DocumentVerificationException { + innovatricsApiService.createDocument(customerId, documentType, ownerId); + } + + /** + * Upload a page of a document to a customer. + * @param customerId id of the customer to whom upload the document page. + * @param page SubmittedDocument object representing the page. + * @param ownerId owner identification. + * @return CreateDocumentPageResponse containing info about the document type. An unsuccessful response will contain an error code. + * @throws RemoteCommunicationException if the document page was not uploaded properly. + */ + private CreateDocumentPageResponse provideDocumentPage(final String customerId, final SubmittedDocument page, final OwnerId ownerId) throws RemoteCommunicationException { + return innovatricsApiService.provideDocumentPage(customerId, page.getSide(), page.getPhoto().getData(), ownerId); + } + + /** + * Checks if CreateDocumentPageResponse contains error or warnings. + * @param pageResponse response to a page upload. + * @return true if there is an error or warnings, false otherwise. + */ + private static boolean containsError(CreateDocumentPageResponse pageResponse) { + return pageResponse.getErrorCode() != null || !CollectionUtils.isEmpty(pageResponse.getWarnings()); + } + + /** + * Creates DocumentSubmitResult with error or reject reason. + * @param uploadId external id of the document. + * @param response returned from provider. + * @return DocumentSubmitResult with error or reject reason. + * @throws DocumentVerificationException in case of rejection reason serialization error. + */ + private DocumentSubmitResult createErrorSubmitResult(String uploadId, CreateDocumentPageResponse response, SubmittedDocument submitted) throws DocumentVerificationException { + final DocumentSubmitResult result = new DocumentSubmitResult(); + result.setUploadId(uploadId); + result.setDocumentId(submitted.getDocumentId()); + + final List rejectionReasons = new ArrayList<>(); + if (response.getErrorCode() != null) { + switch (response.getErrorCode()) { + case NO_CARD_CORNERS_DETECTED -> rejectionReasons.add("Document page was not detected in the photo."); + case PAGE_DOESNT_MATCH_DOCUMENT_TYPE_OF_PREVIOUS_PAGE -> rejectionReasons.add("Mismatched document pages types."); + default -> rejectionReasons.add("Unknown error: %s".formatted(response.getErrorCode().getValue())); + } + } + + if (!CollectionUtils.isEmpty(response.getWarnings())) { + for (CreateDocumentPageResponse.WarningsEnum warning : response.getWarnings()) { + switch (warning) { + case DOCUMENT_TYPE_NOT_RECOGNIZED -> rejectionReasons.add("Document type not recognized."); + default -> rejectionReasons.add("Unknown warning: %s".formatted(warning.getValue())); + } + } + } + + if (!rejectionReasons.isEmpty()) { + result.setRejectReason(serializeToString(rejectionReasons)); + } + + return result; + } + + /** + * Gets all customer data extracted from uploaded documents in a JSON form. + * @param customerId id of the customer. + * @param ownerId owner identification. + * @return JSON serialized data. + * @throws RemoteCommunicationException in case of the remote service error. + * @throws DocumentVerificationException if the returned data could not be provided. + */ + private String getExtractedData(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException, DocumentVerificationException { + return serializeToString(innovatricsApiService.getCustomer(customerId, ownerId)); } + + /** + * Checks if a document portrait of the customer is available. + * @param customerId id of the customer. + * @param ownerId owner identification. + * @return true if document portrait is available, false otherwise. + */ + private boolean hasDocumentPortrait(final String customerId, final OwnerId ownerId) throws RemoteCommunicationException { + return innovatricsApiService.getDocumentPortrait(customerId, ownerId).isPresent(); + } + + /** + * Creates DocumentSubmitResult containing extracted data. + * @param customerId id of the customer to get data from. + * @return DocumentSubmitResult containing extracted data. + */ + private static DocumentSubmitResult createSubmitResult(final String customerId, final SubmittedDocument submitted) { + final DocumentSubmitResult result = new DocumentSubmitResult(); + result.setUploadId(customerId); + result.setDocumentId(submitted.getDocumentId()); + result.setExtractedData(DocumentSubmitResult.NO_DATA_EXTRACTED); + return result; + } + + /** + * Gets document inspection from Innovatrics and parses it to the verification result. + * @param customerId id of the customer the document belongs to. + * @param ownerId owner identification. + * @return DocumentVerificationResult + */ + private DocumentVerificationResult createVerificationResult(String customerId, OwnerId ownerId) throws DocumentVerificationException, RemoteCommunicationException { + final DocumentInspectResponse response = innovatricsApiService.inspectDocument(customerId, ownerId); + + final List rejectionReasons = new ArrayList<>(); + if (Boolean.TRUE.equals(response.getExpired())) { + rejectionReasons.add("Document expired."); + } + + if (response.getMrzInspection() != null && !Boolean.TRUE.equals(response.getMrzInspection().getValid())) { + rejectionReasons.add("MRZ does not conform the ICAO specification."); + } + + final VisualZoneInspection viz = response.getVisualZoneInspection(); + rejectionReasons.addAll(parseVisualZoneInspection(viz)); + + if (response.getPageTampering() != null) { + response.getPageTampering().forEach((side, inspection) -> { + if (Boolean.TRUE.equals(inspection.getColorProfileChangeDetected())) { + rejectionReasons.add("Colors on the document %s does not corresponds to the expected color profile.".formatted(side)); + } + if (Boolean.TRUE.equals(inspection.getLooksLikeScreenshot())) { + rejectionReasons.add("Provided image of the document %s was taken from a screen of another device.".formatted(side)); + } + if (Boolean.TRUE.equals(inspection.getTamperedTexts())) { + rejectionReasons.add("Text of the document %s is tampered.".formatted(side)); + } + }); + } + + final DocumentVerificationResult result = new DocumentVerificationResult(); + result.setUploadId(customerId); + result.setVerificationResult(serializeToString(response)); + if (!rejectionReasons.isEmpty()) { + result.setRejectReason(serializeToString(rejectionReasons)); + } + return result; + } + + /** + * Parse VisualZoneInspection of a document provided by Innovatrics. + * @param visualZoneInspection inspection of a document by Innovatrics. + * @return List of reasons to reject the document. + */ + private List parseVisualZoneInspection(final VisualZoneInspection visualZoneInspection) { + final List rejectionReasons = new ArrayList<>(); + if (visualZoneInspection == null) { + return rejectionReasons; + } + + // Contains fields with a ocr confidence lower than ocr-text-field-threshold settings. + final List lowOcrConfidenceAttributes = visualZoneInspection.getOcrConfidence().getLowOcrConfidenceTexts(); + if (!CollectionUtils.isEmpty(lowOcrConfidenceAttributes)) { + rejectionReasons.add("Low OCR confidence of attributes: %s".formatted(lowOcrConfidenceAttributes)); + } + + final TextConsistency textConsistency = visualZoneInspection.getTextConsistency(); + if (textConsistency == null) { + return rejectionReasons; + } + + final TextConsistentWith textConsistentWith = textConsistency.getConsistencyWith(); + if (textConsistentWith == null) { + return rejectionReasons; + } + + final MrzConsistency mrzConsistency = textConsistentWith.getMrz(); + if (mrzConsistency != null) { + final List inconsistentAttributes = getCrucial(mrzConsistency.getInconsistentTexts()); + if (!inconsistentAttributes.isEmpty()) { + rejectionReasons.add("Inconsistent crucial attributes with MRZ: %s".formatted(inconsistentAttributes)); + } + } + + final BarcodesConsistency barcodesConsistency = textConsistentWith.getBarcodes(); + if (barcodesConsistency != null) { + final List inconsistentAttributes = getCrucial(barcodesConsistency.getInconsistentTexts()); + if (!inconsistentAttributes.isEmpty()) { + rejectionReasons.add("Inconsistent crucial attributes with barcode: %s".formatted(inconsistentAttributes)); + } + } + + return rejectionReasons; + } + + /** + * Intersects list of attributes with CRUCIAL_ATTRIBUTES. + * @param attributes Attributes to do the intersection on. + * @return Attributes intersection. + */ + private List getCrucial(final List attributes) { + if (attributes == null) { + return Collections.emptyList(); + } + + final Set crucialFields = configuration.getDocumentVerificationConfiguration().getCrucialFields(); + return attributes.stream().filter(crucialFields::contains).toList(); + } + + private String serializeToString(T src) throws DocumentVerificationException { + try { + return objectMapper.writeValueAsString(src); + } catch (JsonProcessingException e) { + throw new DocumentVerificationException("Unexpected error when serializing data", e); + } + } + + private T deserializeFromString(String src) throws DocumentVerificationException { + try { + return objectMapper.readValue(src, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + throw new DocumentVerificationException("Unexpected error when deserializing data", e); + } + } + } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java index 833396add..8b97aa52e 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsLivenessService.java @@ -33,7 +33,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,11 +46,11 @@ @Transactional(readOnly = true) @Slf4j @AllArgsConstructor -@ConditionalOnProperty(value = "enrollment-server-onboarding.presence-check.provider", havingValue = "innovatrics") +@ConditionalOnExpression(""" + '${enrollment-server-onboarding.presence-check.provider}' == 'innovatrics' and ${enrollment-server-onboarding.onboarding-process.enabled} == true + """) class InnovatricsLivenessService { - private static final String INNOVATRICS_CUSTOMER_ID = "InnovatricsCustomerId"; - private final InnovatricsApiService innovatricsApiService; private final IdentityVerificationRepository identityVerificationRepository; @@ -124,8 +124,7 @@ private static String fetchCustomerId(final OwnerId id, final IdentityVerificati throw new IdentityVerificationException("Unable to deserialize session info", e); } - // TODO (racansky, 2023-11-28) discuss the format with Jan Pesek, extract to common logic - final String customerId = (String) sessionInfo.getSessionAttributes().get(INNOVATRICS_CUSTOMER_ID); + final String customerId = (String) sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE); if (Strings.isNullOrEmpty(customerId)) { throw new IdentityVerificationException("Missing a customer ID value for calling Innovatrics, " + id); } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java index 7899e776c..007c06c6a 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProvider.java @@ -49,8 +49,6 @@ @AllArgsConstructor class InnovatricsPresenceCheckProvider implements PresenceCheckProvider { - private static final String INNOVATRICS_CUSTOMER_ID = "InnovatricsCustomerId"; - private final InnovatricsApiService innovatricsApiService; private final InnovatricsConfigProps configuration; @@ -61,8 +59,8 @@ public void initPresenceCheck(final OwnerId id, final Image photo) { } @Override - public boolean shouldProvideTrustedPhoto() { - return false; + public TrustedPhotoSource trustedPhotoSource() { + return TrustedPhotoSource.REFERENCE; } @Override @@ -152,8 +150,7 @@ private static Optional fail(final String errorDetail) { } private static String fetchCustomerId(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException { - // TODO (racansky, 2023-11-28) discuss the format with Jan Pesek - final String customerId = (String) sessionInfo.getSessionAttributes().get(INNOVATRICS_CUSTOMER_ID); + final String customerId = (String) sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE); if (Strings.isNullOrEmpty(customerId)) { throw new PresenceCheckException("Missing a customer ID value for calling Innovatrics, " + id); } @@ -164,12 +161,7 @@ private static String fetchCustomerId(final OwnerId id, final SessionInfo sessio public void cleanupIdentityData(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException { logger.info("Invoked cleanupIdentityData, {}", id); final String customerId = fetchCustomerId(id, sessionInfo); - - innovatricsApiService.deleteLiveness(customerId, id); - logger.debug("Deleted liveness, {}", id); - - innovatricsApiService.deleteSelfie(customerId, id); - logger.debug("Deleted selfie, {}", id); + innovatricsApiService.deleteCustomer(customerId, id); } record PresenceCheckError(PresenceCheckStatus status, String rejectReason, String errorDetail){} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProviderTest.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProviderTest.java new file mode 100644 index 000000000..fa9384eba --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProviderTest.java @@ -0,0 +1,174 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.JsonPath; +import com.wultra.app.enrollmentserver.model.enumeration.CardSide; +import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; +import com.wultra.app.enrollmentserver.model.enumeration.DocumentVerificationStatus; +import com.wultra.app.enrollmentserver.model.integration.*; +import com.wultra.app.enrollmentserver.model.integration.Image; +import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.util.StringUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Test of {@link InnovatricsDocumentVerificationProvider}. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@SpringBootTest(classes = EnrollmentServerTestApplication.class) +@ActiveProfiles("test") +class InnovatricsDocumentVerificationProviderTest { + + @Autowired + private InnovatricsDocumentVerificationProvider tested; + + @MockBean + private InnovatricsApiService apiService; + + @Test + void testSubmitDocuments() throws Exception { + final OwnerId ownerId = createOwnerId(); + when(apiService.createCustomer(ownerId)).thenReturn(new CreateCustomerResponse("c123")); + + final Links docLink = new Links("docResource"); + final CreateDocumentResponse documentResponse = new CreateDocumentResponse(); + documentResponse.setLinks(docLink); + when(apiService.createDocument("c123", DocumentType.PASSPORT, ownerId)).thenReturn(documentResponse); + + final CreateDocumentPageResponse pageResponse = new CreateDocumentPageResponse(); + when(apiService.provideDocumentPage("c123", CardSide.FRONT, "img".getBytes(), ownerId)).thenReturn(pageResponse); + + final BiometricMultiValueAttribute ageAttr = new BiometricMultiValueAttribute("42", null, null, null, "40"); + final MultiValueAttribute surnameAttr = new MultiValueAttribute("SPECIMEN", null, null, null); + final Customer customer = new Customer(); + customer.age(ageAttr).setSurname(surnameAttr); + final GetCustomerResponse customerResponse = new GetCustomerResponse(); + customerResponse.customer(customer); + when(apiService.getCustomer("c123", ownerId)).thenReturn(customerResponse); + + final SubmittedDocument doc = new SubmittedDocument(); + doc.setType(DocumentType.PASSPORT); + doc.setSide(CardSide.FRONT); + doc.setPhoto(Image.builder().data("img".getBytes()).build()); + + final DocumentsSubmitResult results = tested.submitDocuments(ownerId, List.of(doc)); + verify(apiService).getCustomer("c123", ownerId); + assertEquals(1, results.getResults().size()); + + final DocumentSubmitResult result = results.getResults().get(0); + assertEquals("c123", result.getUploadId()); + assertFalse(StringUtils.hasText(result.getErrorDetail())); + assertFalse(StringUtils.hasText(result.getRejectReason())); + assertNotNull(result.getExtractedData()); + + assertEquals("42", JsonPath.read(result.getExtractedData(), "$.customer.age.visualZone")); + assertEquals("40", JsonPath.read(result.getExtractedData(), "$.customer.age.documentPortrait")); + assertEquals("SPECIMEN", JsonPath.read(result.getExtractedData(), "$.customer.surname.visualZone")); + } + + @Test + void testSubmitDocument_handleProvideDocumentPageError() throws Exception { + final OwnerId ownerId = createOwnerId(); + when(apiService.createCustomer(ownerId)).thenReturn(new CreateCustomerResponse("c123")); + + final Links docLink = new Links("docResource"); + final CreateDocumentResponse documentResponse = new CreateDocumentResponse(); + documentResponse.setLinks(docLink); + when(apiService.createDocument("c123", DocumentType.PASSPORT, ownerId)).thenReturn(documentResponse); + + final CreateDocumentPageResponse pageResponse = new CreateDocumentPageResponse( + "front", + CreateDocumentPageResponse.ErrorCodeEnum.NO_CARD_CORNERS_DETECTED, + List.of(CreateDocumentPageResponse.WarningsEnum.DOCUMENT_TYPE_NOT_RECOGNIZED)); + when(apiService.provideDocumentPage("c123", CardSide.FRONT, "img".getBytes(), ownerId)).thenReturn(pageResponse); + + final SubmittedDocument doc = new SubmittedDocument(); + doc.setType(DocumentType.PASSPORT); + doc.setSide(CardSide.FRONT); + doc.setPhoto(Image.builder().data("img".getBytes()).build()); + + final DocumentsSubmitResult results = tested.submitDocuments(ownerId, List.of(doc)); + verify(apiService).provideDocumentPage("c123", CardSide.FRONT, "img".getBytes(), ownerId); + assertEquals(1, results.getResults().size()); + + final DocumentSubmitResult result = results.getResults().get(0); + assertEquals("c123", result.getUploadId()); + assertTrue(StringUtils.hasText(result.getRejectReason())); + } + + @Test + void testParseRejectionReason() throws Exception { + final DocumentResultEntity entity = new DocumentResultEntity(); + entity.setRejectReason(new ObjectMapper().writeValueAsString(List.of("Reason1", "Reason2"))); + assertEquals(List.of("Reason1", "Reason2"), tested.parseRejectionReasons(entity)); + } + + @Test + void testParseEmptyRejectionReason() throws Exception { + final DocumentResultEntity entity = new DocumentResultEntity(); + assertTrue(tested.parseRejectionReasons(entity).isEmpty()); + } + + @Test + void testVerifyDocuments() throws Exception { + final OwnerId ownerId = createOwnerId(); + final DocumentInspectResponse response = new DocumentInspectResponse(); + when(apiService.inspectDocument("c123", ownerId)).thenReturn(response); + + final DocumentsVerificationResult result = tested.verifyDocuments(ownerId, List.of("c123")); + assertTrue(result.isAccepted()); + assertEquals("c123", result.getResults().get(0).getUploadId()); + assertNotNull(result.getVerificationId()); + assertNotNull(result.getResults().get(0).getVerificationResult()); + } + + @Test + void testVerifyDocuments_expired() throws Exception { + final OwnerId ownerId = createOwnerId(); + final DocumentInspectResponse response = new DocumentInspectResponse(true, null); + when(apiService.inspectDocument("c123", ownerId)).thenReturn(response); + + final DocumentsVerificationResult result = tested.verifyDocuments(ownerId, List.of("c123")); + assertEquals(DocumentVerificationStatus.REJECTED, result.getStatus()); + assertEquals(List.of("Document expired."), new ObjectMapper().readValue(result.getRejectReason(), new TypeReference>() {})); + assertEquals("c123", result.getResults().get(0).getUploadId()); + assertNotNull(result.getVerificationId()); + } + + private OwnerId createOwnerId() { + final OwnerId ownerId = new OwnerId(); + ownerId.setUserId("joe"); + ownerId.setActivationId("a123"); + return ownerId; + } + +} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java index 7037ca408..ed57e1559 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsPresenceCheckProviderTest.java @@ -148,7 +148,7 @@ void testGetResult_customerInspectionRejected() throws Exception { private static SessionInfo createSessionInfo() { final SessionInfo sessionInfo = new SessionInfo(); - sessionInfo.setSessionAttributes(Map.of("InnovatricsCustomerId", CUSTOMER_ID)); + sessionInfo.setSessionAttributes(Map.of("primaryDocumentReference", CUSTOMER_ID)); return sessionInfo; } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsRestApiServiceTest.java b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsRestApiServiceTest.java new file mode 100644 index 000000000..f6f065984 --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/test/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsRestApiServiceTest.java @@ -0,0 +1,138 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.provider.innovatrics; + +import com.wultra.app.enrollmentserver.model.enumeration.CardSide; +import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateCustomerResponse; +import com.wultra.app.onboardingserver.provider.innovatrics.model.api.CreateDocumentPageResponse; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; + +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test of {@link InnovatricsApiService}. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@SpringBootTest( + classes = EnrollmentServerTestApplication.class, + properties = { + "enrollment-server-onboarding.provider.innovatrics.serviceBaseUrl=http://localhost:" + InnovatricsRestApiServiceTest.PORT + }) +@ActiveProfiles("test") +class InnovatricsRestApiServiceTest { + + static final int PORT = 52936; + + @Autowired + private InnovatricsApiService tested; + + private MockWebServer mockWebServer; + + @BeforeEach + void setup() throws Exception { + mockWebServer = new MockWebServer(); + mockWebServer.start(PORT); + } + + @AfterEach + void cleanup() throws Exception { + mockWebServer.shutdown(); + } + + @Test + void testCreateCustomer() throws Exception { + final OwnerId ownerId = createOwnerId(); + mockWebServer.enqueue(new MockResponse() + .setHeader("Content-Type", MediaType.APPLICATION_JSON) + // Real response to POST /api/v1/customers + .setBody(""" + { + "id": "c2e91b1f-0ccb-4ba0-93ae-d255a2a443af", + "links": { + "self": "/api/v1/customers/c2e91b1f-0ccb-4ba0-93ae-d255a2a443af" + } + } + """) + .setResponseCode(HttpStatus.OK.value())); + + final CreateCustomerResponse response = tested.createCustomer(ownerId); + assertEquals("c2e91b1f-0ccb-4ba0-93ae-d255a2a443af", response.getId()); + assertEquals("/api/v1/customers/c2e91b1f-0ccb-4ba0-93ae-d255a2a443af", response.getLinks().getSelf()); + } + + @Test + void testErrorResponse() { + final OwnerId ownerId = createOwnerId(); + mockWebServer.enqueue(new MockResponse() + .setHeader("Content-Type", MediaType.APPLICATION_JSON) + // Real response to uploading a page without previous document resource creation + .setBody(""" + { + "errorCode": "NOT_FOUND", + "errorMessage": "string" + } + """) + .setResponseCode(500)); + + assertThrows(RemoteCommunicationException.class, () -> tested.createCustomer(ownerId)); + } + + @Test + void testNonMatchingPageType() throws Exception { + final OwnerId ownerId = createOwnerId(); + mockWebServer.enqueue(new MockResponse() + .setHeader("Content-Type", MediaType.APPLICATION_JSON) + // Real response to uploading a second page that is different from the first one + .setBody(""" + { + "errorCode": "PAGE_DOESNT_MATCH_DOCUMENT_TYPE_OF_PREVIOUS_PAGE" + } + """) + .setResponseCode(HttpStatus.OK.value())); + + final CreateDocumentPageResponse response = tested.provideDocumentPage("123", CardSide.FRONT, "data".getBytes(), ownerId); + assertNotNull(response.getErrorCode()); + + final RecordedRequest recordedRequest = mockWebServer.takeRequest(1L, TimeUnit.SECONDS); + assertNotNull(recordedRequest); + assertEquals("PUT /api/v1/customers/123/document/pages HTTP/1.1", recordedRequest.getRequestLine()); + } + + private OwnerId createOwnerId() { + final OwnerId ownerId = new OwnerId(); + ownerId.setUserId("joe"); + ownerId.setActivationId("a123"); + return ownerId; + } + +} diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties new file mode 100644 index 000000000..8e68187cc --- /dev/null +++ b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties @@ -0,0 +1,8 @@ +enrollment-server-onboarding.document-verification.provider=innovatrics +enrollment-server-onboarding.presence-check.provider=innovatrics +enrollment-server-onboarding.onboarding-process.enabled=false + +enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE,SVK +enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames + +enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize=1048576 \ No newline at end of file diff --git a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java index 8e85e5f66..d34579224 100644 --- a/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java +++ b/enrollment-server-onboarding-provider-iproov/src/main/java/com/wultra/app/onboardingserver/provider/iproov/IProovPresenceCheckProvider.java @@ -127,8 +127,8 @@ public void initPresenceCheck(final OwnerId id, final Image photo) throws Presen } @Override - public boolean shouldProvideTrustedPhoto() { - return true; + public TrustedPhotoSource trustedPhotoSource() { + return TrustedPhotoSource.IMAGE; } @Override diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java index bddd9e43c..692830985 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProvider.java @@ -104,7 +104,11 @@ public DocumentsSubmitResult checkDocumentUpload(OwnerId id, DocumentVerificatio } @Override - public DocumentsSubmitResult submitDocuments(OwnerId id, List documents) { + public DocumentsSubmitResult submitDocuments(OwnerId id, List documents) throws DocumentVerificationException { + if (documents.stream().anyMatch(doc -> "throw.exception".equals(doc.getPhoto().getFilename()))) { + throw new DocumentVerificationException("Filename to throw an exception is present in documents."); + } + final List submitResults = documents.stream() .map(this::toDocumentSubmitResult) .toList(); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java index 86323a35f..12471f923 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckService.java @@ -44,10 +44,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.*; @@ -62,9 +59,6 @@ @Slf4j public class PresenceCheckService { - private static final String SESSION_ATTRIBUTE_TIMESTAMP_LAST_USED = "timestampLastUsed"; - private static final String SESSION_ATTRIBUTE_IMAGE_UPLOADED = "imageUploaded"; - private final IdentityVerificationConfig identityVerificationConfig; private final DocumentVerificationRepository documentVerificationRepository; private final DocumentProcessingService documentProcessingService; @@ -128,7 +122,7 @@ public void checkPresenceVerification( final OwnerId ownerId, final IdentityVerificationEntity idVerification) throws PresenceCheckException, RemoteCommunicationException { - final SessionInfo sessionInfo = updateSessionInfo(ownerId, idVerification, Map.of(SESSION_ATTRIBUTE_TIMESTAMP_LAST_USED, ownerId.getTimestamp())); + final SessionInfo sessionInfo = updateSessionInfo(ownerId, idVerification, Map.of(SessionInfo.ATTRIBUTE_TIMESTAMP_LAST_USED, ownerId.getTimestamp())); final PresenceCheckResult result = presenceCheckProvider.getResult(ownerId, sessionInfo); auditService.auditPresenceCheckProvider(idVerification, "Got presence check result: {} for user: {}", result.getStatus(), ownerId.getUserId()); @@ -216,15 +210,16 @@ private void initPresentCheckWithImage(final OwnerId ownerId, final IdentityVeri } final Optional photo = fetchTrustedPhoto(ownerId, idVerification); + setIdentityDocumentReferences(ownerId, idVerification); presenceCheckProvider.initPresenceCheck(ownerId, photo.orElse(null)); logger.info("Presence check initialized, {}", ownerId); - updateSessionInfo(ownerId, idVerification, Map.of(SESSION_ATTRIBUTE_IMAGE_UPLOADED, true)); + updateSessionInfo(ownerId, idVerification, Map.of(SessionInfo.ATTRIBUTE_IMAGE_UPLOADED, true)); auditService.auditPresenceCheckProvider(idVerification, "Presence check initialized for user: {}", ownerId.getUserId()); } } private Optional fetchTrustedPhoto(final OwnerId ownerId, final IdentityVerificationEntity idVerification) throws DocumentVerificationException, RemoteCommunicationException, PresenceCheckException { - if (presenceCheckProvider.shouldProvideTrustedPhoto()) { + if (PresenceCheckProvider.TrustedPhotoSource.IMAGE == presenceCheckProvider.trustedPhotoSource()) { final Image photo = fetchTrustedPhotoFromDocumentVerifier(ownerId, idVerification); final Image upscaledPhoto = imageProcessor.upscaleImage(ownerId, photo, identityVerificationConfig.getMinimalSelfieWidth()); return Optional.of(upscaledPhoto); @@ -233,6 +228,22 @@ private Optional fetchTrustedPhoto(final OwnerId ownerId, final IdentityV } } + private void setIdentityDocumentReferences(final OwnerId ownerId, final IdentityVerificationEntity idVerification) throws DocumentVerificationException, PresenceCheckException { + if (PresenceCheckProvider.TrustedPhotoSource.REFERENCE != presenceCheckProvider.trustedPhotoSource()) { + return; + } + + final List docsWithPhoto = getDocsWithPhoto(idVerification, ownerId); + final String primaryDocReference = getPreferredDocWithPhoto(docsWithPhoto, ownerId).getPhotoId(); + final List otherDocsReferences = docsWithPhoto.stream() + .map(DocumentVerificationEntity::getPhotoId) + .filter(id -> !Objects.equals(id, primaryDocReference)) + .distinct().toList(); + + updateSessionInfo(ownerId, idVerification, Map.of(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE, primaryDocReference, + SessionInfo.ATTRIBUTE_OTHER_DOCUMENTS_REFERENCES, otherDocsReferences)); + } + /** * Starts new presence check process. * @@ -264,6 +275,15 @@ private SessionInfo startPresenceCheck(OwnerId ownerId, IdentityVerificationEnti protected Image fetchTrustedPhotoFromDocumentVerifier(final OwnerId ownerId, final IdentityVerificationEntity idVerification) throws DocumentVerificationException, RemoteCommunicationException { + final List docsWithPhoto = getDocsWithPhoto(idVerification, ownerId); + final DocumentVerificationEntity preferredDocWithPhoto = getPreferredDocWithPhoto(docsWithPhoto, ownerId); + logger.info("Selected {} as the source of person photo, {}", preferredDocWithPhoto, ownerId); + final String photoId = preferredDocWithPhoto.getPhotoId(); + return identityVerificationService.getPhotoById(photoId, ownerId); + } + + private List getDocsWithPhoto(final IdentityVerificationEntity idVerification, + final OwnerId ownerId) throws DocumentVerificationException { final List docsWithPhoto = documentVerificationRepository.findAllWithPhoto(idVerification); if (docsWithPhoto.isEmpty()) { throw new DocumentVerificationException("Unable to initialize presence check - missing person photo, " + ownerId); @@ -273,23 +293,23 @@ protected Image fetchTrustedPhotoFromDocumentVerifier(final OwnerId ownerId, fin Preconditions.checkNotNull(docWithPhoto.getPhotoId(), "Expected photoId value in " + docWithPhoto) ); - DocumentVerificationEntity preferredDocWithPhoto = null; + return docsWithPhoto; + } + + private DocumentVerificationEntity getPreferredDocWithPhoto(final List docsWithPhoto, + final OwnerId ownerId) { + for (DocumentType documentType : DocumentType.PREFERRED_SOURCE_OF_PERSON_PHOTO) { Optional docEntity = docsWithPhoto.stream() .filter(value -> value.getType() == documentType) .findFirst(); if (docEntity.isPresent()) { - preferredDocWithPhoto = docEntity.get(); - break; + return docEntity.get(); } } - if (preferredDocWithPhoto == null) { - logger.warn("Unable to select a source of person photo to initialize presence check, {}", ownerId); - preferredDocWithPhoto = docsWithPhoto.get(0); - } - logger.info("Selected {} as the source of person photo, {}", preferredDocWithPhoto, ownerId); - String photoId = preferredDocWithPhoto.getPhotoId(); - return identityVerificationService.getPhotoById(photoId, ownerId); + + logger.warn("Unable to select a source of person photo to initialize presence check, {}", ownerId); + return docsWithPhoto.get(0); } private void evaluatePresenceCheckResult(OwnerId ownerId, @@ -389,6 +409,6 @@ private boolean imageAlreadyUploaded(final IdentityVerificationEntity identityVe final SessionInfo sessionInfo = jsonSerializationService.deserialize(sessionInfoString, SessionInfo.class); return sessionInfo != null && !CollectionUtils.isEmpty(sessionInfo.getSessionAttributes()) - && Boolean.TRUE.equals(sessionInfo.getSessionAttributes().get(SESSION_ATTRIBUTE_IMAGE_UPLOADED)); + && Boolean.TRUE.equals(sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_IMAGE_UPLOADED)); } } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java index 948919729..7dae9452b 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingService.java @@ -17,6 +17,7 @@ */ package com.wultra.app.onboardingserver.impl.service.document; +import com.google.common.base.Strings; import com.wultra.app.enrollmentserver.api.model.onboarding.request.DocumentSubmitRequest; import com.wultra.app.enrollmentserver.model.Document; import com.wultra.app.enrollmentserver.model.DocumentMetadata; @@ -43,10 +44,9 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; + +import static java.util.stream.Collectors.groupingBy; /** * Service implementing document processing features. @@ -119,22 +119,44 @@ public List submitDocuments( DocumentSubmitRequest request, OwnerId ownerId) throws DocumentSubmitException { - List documents = getDocuments(ownerId, request); - - List docVerifications = new ArrayList<>(); - List docResults = new ArrayList<>(); + checkDocumentResubmit(ownerId, request); + final List documents = getDocuments(ownerId, request); + final var documentsByType = request.getDocuments().stream() + .collect(groupingBy(DocumentSubmitRequest.DocumentMetadata::getType)); - List docsMetadata = request.getDocuments(); - for (DocumentSubmitRequest.DocumentMetadata docMetadata : docsMetadata) { - DocumentVerificationEntity docVerification = createDocumentVerification(ownerId, idVerification, docMetadata); - docVerification.setIdentityVerification(idVerification); - docVerifications.add(docVerification); + final List docVerifications = new ArrayList<>(); + for (var docMetadataList : documentsByType.values()) { + docVerifications.addAll(submitDocument(docMetadataList, documents, idVerification, ownerId)); + } + return docVerifications; + } - checkDocumentResubmit(ownerId, request, docVerification); + /** + * Submit pages of a document to document verify provider. + * @param pagesMetadata Pages metadata from request. + * @param pagesData Pages data. + * @param idVerification Identity verification entity. + * @param ownerId Owner identification. + * @return + */ + private List submitDocument(final List pagesMetadata, + final List pagesData, + final IdentityVerificationEntity idVerification, + final OwnerId ownerId) { + + // Maps are used to associate DocumentsSubmitResult - DocumentVerificationEntities - DocumentMetadata + final Map docVerifications = new HashMap<>(); + final Map docMetadataMap = new HashMap<>(); + + final List submittedDocuments = new ArrayList<>(); + for (var metadata : pagesMetadata) { + final DocumentVerificationEntity docVerification = createDocumentVerification(ownerId, idVerification, metadata); + docVerifications.put(docVerification.getId(), docVerification); + docMetadataMap.put(docVerification.getId(), metadata); + handleResubmit(ownerId, metadata.getOriginalDocumentId(), docVerification); - SubmittedDocument submittedDoc; try { - submittedDoc = createSubmittedDocument(ownerId, docMetadata, documents); + submittedDocuments.add(createSubmittedDocument(ownerId, metadata, pagesData, docVerification)); } catch (DocumentSubmitException e) { logger.warn("Document verification ID: {}, failed: {}", docVerification.getId(), e.getMessage()); logger.debug("Document verification ID: {}, failed", docVerification.getId(), e); @@ -142,60 +164,98 @@ public List submitDocuments( docVerification.setErrorDetail(ErrorDetail.DOCUMENT_VERIFICATION_FAILED); docVerification.setErrorOrigin(ErrorOrigin.DOCUMENT_VERIFICATION); auditService.audit(docVerification, "Document verification failed for user: {}", ownerId.getUserId()); - return docVerifications; + return docVerifications.values().stream().toList(); } + } - DocumentSubmitResult docSubmitResult = submitDocumentToProvider(ownerId, docVerification, submittedDoc); + final List docVerificationsList = docVerifications.values().stream().toList(); + final DocumentsSubmitResult results = submitDocumentToProvider(submittedDocuments, docVerificationsList, idVerification, ownerId); + processSubmitResults(results, docVerifications, ownerId); - // TODO - after synchronous submission to document verification provider the document state should be - // set to VERIFICATION_PENDING, for asynchronous processing the UPLOAD_IN_PROGRESS state should remain + docVerificationsList.stream() + .filter(doc -> StringUtils.isNotBlank(doc.getUploadId())) + .map(doc -> docMetadataMap.get(doc.getId()).getUploadId()) + .filter(StringUtils::isNotBlank) + .forEach(fileUploadId -> { + documentDataRepository.deleteById(fileUploadId); + logger.info("Deleted stored document data with id={}, {}", fileUploadId, ownerId); + }); - DocumentResultEntity docResult = createDocumentResult(docVerification, docSubmitResult); - docResult.setTimestampCreated(ownerId.getTimestamp()); + return docVerificationsList; + } - docResults.add(docResult); + /** + * Process submit results. + * @param results Document submit result from provider. + * @param docVerificationsMap To pair results with corresponding DocumentVerificationEntity. + * @param ownerId Owner identification. + */ + private void processSubmitResults(final DocumentsSubmitResult results, + final Map docVerificationsMap, + final OwnerId ownerId) { - // Delete previously persisted document after a successful upload to the provider - if (docVerification.getUploadId() != null && docMetadata.getUploadId() != null) { - documentDataRepository.deleteById(docMetadata.getUploadId()); - } - } + final List docResults = new ArrayList<>(); - documentVerificationRepository.saveAll(docVerifications); + for (final DocumentSubmitResult result : results.getResults()) { + final DocumentVerificationEntity docVerification = docVerificationsMap.get(result.getDocumentId()); + processDocsSubmitResults(ownerId, docVerification, results, result); - for (int i = 0; i < docVerifications.size(); i++) { - DocumentVerificationEntity docVerificationEntity = docVerifications.get(i); - docResults.get(i).setDocumentVerification(docVerificationEntity); + final DocumentResultEntity docResult = createDocumentResult(docVerification, result); + docResult.setTimestampCreated(ownerId.getTimestamp()); + docResult.setDocumentVerification(docVerification); + + docResults.add(docResult); } - documentResultRepository.saveAll(docResults); - return docVerifications; + documentVerificationRepository.saveAll(docVerificationsMap.values()); + documentResultRepository.saveAll(docResults); + logger.debug("Processed submit result of documents {}, {}", docVerificationsMap.values(), ownerId); } - public void checkDocumentResubmit(OwnerId ownerId, - DocumentSubmitRequest request, - DocumentVerificationEntity docVerification) throws DocumentSubmitException { - if (request.isResubmit() && docVerification.getOriginalDocumentId() == null) { - throw new DocumentSubmitException( - String.format("Detected a resubmit request without specified originalDocumentId for %s, %s", docVerification, ownerId)); - } else if (request.isResubmit()) { - Optional originalDocOptional = - documentVerificationRepository.findById(docVerification.getOriginalDocumentId()); - if (originalDocOptional.isEmpty()) { - logger.warn("Missing previous DocumentVerificationEntity(originalDocumentId={}), {}", - docVerification.getOriginalDocumentId(), ownerId); - } else { - DocumentVerificationEntity originalDoc = originalDocOptional.get(); - originalDoc.setStatus(DocumentStatus.DISPOSED); - originalDoc.setUsedForVerification(false); - originalDoc.setTimestampDisposed(ownerId.getTimestamp()); - originalDoc.setTimestampLastUpdated(ownerId.getTimestamp()); - logger.info("Replaced previous {} with new {}, {}", originalDocOptional, docVerification, ownerId); - auditService.audit(docVerification, "Document replaced with new one for user: {}", ownerId.getUserId()); + /** + * Validates resubmit parameters of DocumentSubmitRequest. + * @param ownerId Owner identification. + * @param request Request body. + * @throws DocumentSubmitException If request is resubmit without original document ID, or is not resubmit with original document ID + */ + private void checkDocumentResubmit(final OwnerId ownerId, final DocumentSubmitRequest request) throws DocumentSubmitException { + final boolean isResubmit = request.isResubmit(); + for (var metadata : request.getDocuments()) { + final String originalDocumentId = metadata.getOriginalDocumentId(); + if (isResubmit && StringUtils.isBlank(originalDocumentId)) { + logger.debug("Request has resubmit flag but misses originalDocumentId {}, {}", metadata, ownerId); + throw new DocumentSubmitException("Detected a resubmit request without specified originalDocumentId, %s".formatted(ownerId)); + } else if (!isResubmit && StringUtils.isNotBlank(originalDocumentId)) { + logger.debug("Request has originalDocumentId but is not flagged as resubmit {}, {}", metadata, ownerId); + throw new DocumentSubmitException("Detected a submit request with specified originalDocumentId=%s, %s".formatted(originalDocumentId, ownerId)); } - } else if (!request.isResubmit() && docVerification.getOriginalDocumentId() != null) { - throw new DocumentSubmitException( - String.format("Detected a submit request with specified originalDocumentId=%s for %s, %s", docVerification.getOriginalDocumentId(), docVerification, ownerId)); + } + } + + /** + * Sets document with originalDocumentId as disposed. If passed originalDocumentId does not exist, no further action is taken. + * @param ownerId Owner identification. + * @param originalDocumentId Id of the original document. + * @param docVerification Resubmitted document. + */ + private void handleResubmit(final OwnerId ownerId, final String originalDocumentId, final DocumentVerificationEntity docVerification) { + if (Strings.isNullOrEmpty(originalDocumentId)) { + logger.debug("Document {} is not a resubmit {}", docVerification, ownerId); + return; + } + + logger.debug("Document {} is a resubmit, {}", docVerification, ownerId); + final Optional originalDocOptional = documentVerificationRepository.findById(originalDocumentId); + if (originalDocOptional.isEmpty()) { + logger.warn("Missing previous DocumentVerificationEntity(originalDocumentId={}), {}", originalDocumentId, ownerId); + } else { + final DocumentVerificationEntity originalDoc = originalDocOptional.get(); + originalDoc.setStatus(DocumentStatus.DISPOSED); + originalDoc.setUsedForVerification(false); + originalDoc.setTimestampDisposed(ownerId.getTimestamp()); + originalDoc.setTimestampLastUpdated(ownerId.getTimestamp()); + logger.info("Replaced previous {} with new {}, {}", originalDoc, docVerification, ownerId); + auditService.audit(docVerification, "Document replaced with new one for user: {}", ownerId.getUserId()); } } @@ -243,6 +303,41 @@ public void checkDocumentSubmitWithProvider(OwnerId ownerId, DocumentResultEntit processDocsSubmitResults(ownerId, docVerification, docsSubmitResults, docSubmitResult); } + /** + * Pass all pages of a document to document verification provider at a single call. + * @param submittedDocs Document pages to submit. + * @param docVerifications Entities associated with the document pages to submit. + * @param identityVerification Identity verification entity. + * @param ownerId Owner identification. + * @return document submit result + */ + private DocumentsSubmitResult submitDocumentToProvider(final List submittedDocs, + final List docVerifications, + final IdentityVerificationEntity identityVerification, + final OwnerId ownerId) { + + final List docVerificationIds = docVerifications.stream().map(DocumentVerificationEntity::getId).toList(); + + try { + final DocumentsSubmitResult results = documentVerificationProvider.submitDocuments(ownerId, submittedDocs); + logger.debug("Documents {} submitted to provider, {}", docVerifications, ownerId); + auditService.auditDocumentVerificationProvider(identityVerification, "Submit documents for user: {}, document IDs: {}", ownerId.getUserId(), docVerificationIds); + return results; + } catch (DocumentVerificationException | RemoteCommunicationException e) { + logger.warn("Document verification ID: {}, failed: {}", docVerificationIds, e.getMessage()); + logger.debug("Document verification ID: {}, failed", docVerificationIds, e); + final DocumentsSubmitResult results = new DocumentsSubmitResult(); + submittedDocs.forEach(doc -> { + final DocumentSubmitResult result = new DocumentSubmitResult(); + result.setDocumentId(doc.getDocumentId()); + result.setErrorDetail(e.getMessage()); + results.getResults().add(result); + }); + return results; + } + + } + public DocumentSubmitResult submitDocumentToProvider(OwnerId ownerId, DocumentVerificationEntity docVerification, SubmittedDocument submittedDoc) { DocumentsSubmitResult docsSubmitResults; DocumentSubmitResult docSubmitResult; @@ -293,8 +388,8 @@ public void pairTwoSidedDocuments(List documents) { continue; } documents.stream() - .filter(item -> item.getType().equals(document.getType())) - .filter(item -> !item.getSide().equals(document.getSide())) + .filter(item -> item.getType() == document.getType()) + .filter(item -> item.getSide() != document.getSide()) .forEach(item -> { logger.debug("Found other side {} for {}", item, document); item.setOtherSideId(document.getId()); @@ -353,6 +448,7 @@ private DocumentVerificationEntity createDocumentVerification(OwnerId ownerId, I entity.setTimestampCreated(ownerId.getTimestamp()); entity.setUsedForVerification(true); final DocumentVerificationEntity saveEntity = documentVerificationRepository.save(entity); + logger.debug("Created {} for {}", saveEntity, ownerId); auditService.auditDebug(entity, "Document created for user: {}", ownerId.getUserId()); return saveEntity; } @@ -360,13 +456,14 @@ private DocumentVerificationEntity createDocumentVerification(OwnerId ownerId, I private SubmittedDocument createSubmittedDocument( OwnerId ownerId, DocumentSubmitRequest.DocumentMetadata docMetadata, - List docs) throws DocumentSubmitException { + List docs, + DocumentVerificationEntity docVerification) throws DocumentSubmitException { final Image photo = Image.builder() .filename(docMetadata.getFilename()) .build(); SubmittedDocument submittedDoc = new SubmittedDocument(); - submittedDoc.setDocumentId(docMetadata.getUploadId()); + submittedDoc.setDocumentId(docVerification.getId()); submittedDoc.setPhoto(photo); submittedDoc.setSide(docMetadata.getSide()); submittedDoc.setType(docMetadata.getType()); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java index a42241794..4be8ac2f5 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/presencecheck/mock/provider/WultraMockPresenceCheckProvider.java @@ -62,8 +62,8 @@ public void initPresenceCheck(OwnerId id, Image photo) { } @Override - public boolean shouldProvideTrustedPhoto() { - return true; + public TrustedPhotoSource trustedPhotoSource() { + return TrustedPhotoSource.IMAGE; } @Override diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index a21dcc03f..9b608a5d4 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -173,7 +173,11 @@ enrollment-server-onboarding.provider.innovatrics.serviceToken=${INNOVATRICS_SER enrollment-server-onboarding.provider.innovatrics.serviceUserAgent=Wultra/OnboardingServer # Innovatrics presence-check configuration -enrollment-server-onboarding.provider.innovatrics.presenceCheck.score=0.875 +enrollment-server-onboarding.provider.innovatrics.presenceCheckConfiguration.score=0.875 + +# Innovatrics document-verification configuration +enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE +enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames # Innovatrics REST client configuration enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate=false diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java index 721d72e80..bad60c5bd 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/docverify/mock/provider/WultraMockDocumentVerificationProviderTest.java @@ -65,7 +65,7 @@ public void setProvider(WultraMockDocumentVerificationProvider provider) { } @Test - void checkDocumentUploadTest() { + void checkDocumentUploadTest() throws Exception { SubmittedDocument document = createSubmittedDocument(); DocumentsSubmitResult submitResult = provider.submitDocuments(ownerId, List.of(document)); diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java index c5daedb85..c256c6d5d 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java @@ -16,20 +16,39 @@ */ package com.wultra.app.onboardingserver.impl.service; +import com.wultra.app.enrollmentserver.model.enumeration.CardSide; import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; +import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase; +import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.enrollmentserver.model.integration.SessionInfo; +import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; +import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; +import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository; +import com.wultra.app.onboardingserver.common.database.ScaResultRepository; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; +import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; import java.util.List; +import java.util.Optional; +import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.DOCUMENT_VERIFICATION_FINAL; +import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK; +import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.*; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; /** @@ -38,16 +57,23 @@ * @author Lukas Lukovsky, lukas.lukovsky@wultra.com * @author Lubos Racansky, lubos.racansky@wultra.com */ -@ExtendWith(MockitoExtension.class) +@SpringBootTest(classes = EnrollmentServerTestApplication.class) +@ActiveProfiles("test") class PresenceCheckServiceTest { - @Mock + @MockBean private IdentityVerificationService identityVerificationService; - @Mock + @MockBean private DocumentVerificationRepository documentVerificationRepository; - @InjectMocks + @MockBean + private PresenceCheckLimitService presenceCheckLimitService; + + @MockBean + private PresenceCheckProvider presenceCheckProvider; + + @Autowired private PresenceCheckService tested; @Test @@ -94,4 +120,44 @@ void testFetchTrustedPhotoFromDocumentVerifier_unknownDocument() throws Exceptio verify(identityVerificationService, times(1)).getPhotoById(docPhotoUnknown.getPhotoId(), ownerId); } + @Test + void initPresentCheckWithImage_withDocumentReferences() throws Exception { + final OwnerId ownerId = new OwnerId(); + ownerId.setActivationId("a1"); + + final DocumentVerificationEntity page1 = new DocumentVerificationEntity(); + page1.setId("1"); + page1.setType(DocumentType.ID_CARD); + page1.setSide(CardSide.FRONT); + page1.setPhotoId("id_card_portrait"); + + final DocumentVerificationEntity page2 = new DocumentVerificationEntity(); + page2.setId("2"); + page2.setType(DocumentType.ID_CARD); + page2.setSide(CardSide.BACK); + page2.setPhotoId("id_card_portrait"); + + final DocumentVerificationEntity page3 = new DocumentVerificationEntity(); + page3.setId("3"); + page3.setType(DocumentType.DRIVING_LICENSE); + page3.setSide(CardSide.FRONT); + page3.setPhotoId("driving_licence_portrait"); + + when(presenceCheckProvider.trustedPhotoSource()).thenReturn(PresenceCheckProvider.TrustedPhotoSource.REFERENCE); + + final IdentityVerificationEntity identityVerification = new IdentityVerificationEntity(); + identityVerification.setPhase(PRESENCE_CHECK); + identityVerification.setStatus(NOT_INITIALIZED); + + when(documentVerificationRepository.findAllWithPhoto(identityVerification)).thenReturn(List.of(page1, page2, page3)); + when(identityVerificationService.findByOptional(ownerId)).thenReturn(Optional.of(identityVerification)); + when(presenceCheckProvider.startPresenceCheck(ownerId)).thenReturn(new SessionInfo()); + + tested.init(ownerId, "p1"); + + assertTrue(identityVerification.getSessionInfo().contains("\"primaryDocumentReference\":\"id_card_portrait\"")); + assertTrue(identityVerification.getSessionInfo().contains("\"otherDocumentsReferences\":[\"driving_licence_portrait\"]")); + verify(presenceCheckProvider).initPresenceCheck(ownerId, null); + } + } diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java index 92fd2aebf..37309ec05 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.java @@ -17,58 +17,274 @@ */ package com.wultra.app.onboardingserver.impl.service.document; +import com.wultra.app.enrollmentserver.api.model.onboarding.request.DocumentSubmitRequest; +import com.wultra.app.enrollmentserver.model.Document; +import com.wultra.app.enrollmentserver.model.enumeration.*; +import com.wultra.app.enrollmentserver.model.integration.*; +import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; +import com.wultra.app.onboardingserver.common.database.DocumentResultRepository; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; +import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository; +import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; -import com.wultra.app.enrollmentserver.model.enumeration.CardSide; -import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; -import org.junit.jupiter.api.BeforeEach; +import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; +import com.wultra.app.onboardingserver.errorhandling.DocumentSubmitException; +import com.wultra.app.onboardingserver.impl.service.DataExtractionService; import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** * @author Lukas Lukovsky, lukas.lukovsky@wultra.com + * @author Jan Pesek, jan.pesek@wultra.com */ +@SpringBootTest(classes = EnrollmentServerTestApplication.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@ActiveProfiles("test") +@Sql class DocumentProcessingServiceTest { - @InjectMocks - DocumentProcessingService service; + @MockBean + DataExtractionService dataExtractionService; - @Mock + @Autowired + DocumentProcessingService tested; + + @Autowired DocumentVerificationRepository documentVerificationRepository; - @BeforeEach - public void init() { - MockitoAnnotations.openMocks(this); + @Autowired + IdentityVerificationRepository identityVerificationRepository; + + @Autowired + DocumentResultRepository documentResultRepository; + + @Test + @Sql + void testPairTwoSidedDocuments() { + tested.pairTwoSidedDocuments(documentVerificationRepository.findAll()); + assertEquals("2", documentVerificationRepository.findById("1").map(DocumentVerificationEntity::getOtherSideId).get()); + assertEquals("1", documentVerificationRepository.findById("2").map(DocumentVerificationEntity::getOtherSideId).get()); + } + + @Test + void testSubmitDocuments() throws Exception { + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get(); + assertNotNull(identityVerification); + + final List metadata = createIdCardMetadata(); + final List data = createIdCardData(); + final OwnerId ownerId = createOwnerId(); + + final DocumentSubmitRequest request = new DocumentSubmitRequest(); + request.setProcessId("p1"); + request.setResubmit(false); + request.setData("files".getBytes()); + request.setDocuments(metadata); + when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data); + + tested.submitDocuments(identityVerification, request, ownerId); + + final List documents = documentVerificationRepository.findAll(); + assertEquals(2, documents.size()); + assertThat(documents) + .extracting(DocumentVerificationEntity::getSide) + .containsExactlyInAnyOrder(CardSide.FRONT, CardSide.BACK); + assertThat(documents) + .extracting(DocumentVerificationEntity::getStatus) + .containsOnly(DocumentStatus.VERIFICATION_PENDING); + + final List results = new ArrayList<>(); + documentResultRepository.findAll().forEach(results::add); + assertEquals(2, results.size()); + assertThat(results) + .extracting(DocumentResultEntity::getDocumentVerification) + .extracting(DocumentVerificationEntity::getId) + .containsExactlyInAnyOrder(documents.stream().map(DocumentVerificationEntity::getId).toArray(String[]::new)); + assertThat(results) + .extracting(DocumentResultEntity::getPhase) + .containsOnly(DocumentProcessingPhase.UPLOAD); + } + + @Test + void testSubmitDocuments_providerThrows() throws Exception { + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get(); + assertNotNull(identityVerification); + + final List metadata = createIdCardMetadata(); + metadata.get(1).setFilename("throw.exception"); + final List data = createIdCardData(); + data.get(1).setFilename("throw.exception"); + final OwnerId ownerId = createOwnerId(); + + final DocumentSubmitRequest request = new DocumentSubmitRequest(); + request.setProcessId("p1"); + request.setResubmit(false); + request.setData("files".getBytes()); + request.setDocuments(metadata); + when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data); + + tested.submitDocuments(identityVerification, request, ownerId); + + final List documents = documentVerificationRepository.findAll(); + assertEquals(2, documents.size()); + assertThat(documents) + .extracting(DocumentVerificationEntity::getSide) + .containsExactlyInAnyOrder(CardSide.FRONT, CardSide.BACK); + assertThat(documents) + .extracting(DocumentVerificationEntity::getStatus) + .containsOnly(DocumentStatus.FAILED); + + final List results = new ArrayList<>(); + documentResultRepository.findAll().forEach(results::add); + assertEquals(2, results.size()); + assertThat(results) + .extracting(DocumentResultEntity::getErrorDetail) + .containsOnly("documentVerificationFailed"); + assertThat(results) + .extracting(DocumentResultEntity::getErrorOrigin) + .containsOnly(ErrorOrigin.DOCUMENT_VERIFICATION); + } + + @Test + void testSubmitDocuments_missingData() throws Exception { + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get(); + assertNotNull(identityVerification); + + final List metadata = createIdCardMetadata(); + final List data = Collections.emptyList(); + final OwnerId ownerId = createOwnerId(); + + final DocumentSubmitRequest request = new DocumentSubmitRequest(); + request.setProcessId("p1"); + request.setResubmit(false); + request.setData("files".getBytes()); + request.setDocuments(metadata); + when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data); + + tested.submitDocuments(identityVerification, request, ownerId); + + List documents = documentVerificationRepository.findAll(); + assertEquals(1, documents.size()); + assertThat(documents) + .extracting(DocumentVerificationEntity::getStatus) + .containsExactlyInAnyOrder(DocumentStatus.FAILED); + } + + @Test + @Sql + void testResubmitDocuments() throws Exception { + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get(); + assertNotNull(identityVerification); + + final List metadata = createIdCardMetadata(); + metadata.get(0).setOriginalDocumentId("original1"); + metadata.get(1).setOriginalDocumentId("original2"); + final List data = createIdCardData(); + final OwnerId ownerId = createOwnerId(); + + final DocumentSubmitRequest request = new DocumentSubmitRequest(); + request.setProcessId("p1"); + request.setResubmit(true); + request.setData("files".getBytes()); + request.setDocuments(metadata); + when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data); + + tested.submitDocuments(identityVerification, request, ownerId); + List documents = documentVerificationRepository.findAll(); + assertEquals(4, documents.size()); + assertThat(documents) + .extracting(DocumentVerificationEntity::getStatus) + .containsOnly(DocumentStatus.VERIFICATION_PENDING, DocumentStatus.DISPOSED); + } + + @Test + void testResubmitDocuments_missingOriginalDocumentId() throws Exception { + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get(); + assertNotNull(identityVerification); + + final List metadata = createIdCardMetadata(); + final List data = createIdCardData(); + final OwnerId ownerId = createOwnerId(); + + final DocumentSubmitRequest request = new DocumentSubmitRequest(); + request.setProcessId("p1"); + request.setResubmit(true); + request.setData("files".getBytes()); + request.setDocuments(metadata); + when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data); + + final DocumentSubmitException exception = assertThrows(DocumentSubmitException.class, + () -> tested.submitDocuments(identityVerification, request, ownerId)); + assertEquals("Detected a resubmit request without specified originalDocumentId, %s".formatted(ownerId), exception.getMessage()); } @Test - void pairTwoSidedDocumentsTest() { - DocumentVerificationEntity docIdCardFront = new DocumentVerificationEntity(); - docIdCardFront.setId("1"); - docIdCardFront.setType(DocumentType.ID_CARD); - docIdCardFront.setSide(CardSide.FRONT); - - DocumentVerificationEntity docIdCardBack = new DocumentVerificationEntity(); - docIdCardBack.setId("2"); - docIdCardBack.setType(DocumentType.ID_CARD); - docIdCardBack.setSide(CardSide.BACK); - - List documents = List.of(docIdCardFront, docIdCardBack); - - service.pairTwoSidedDocuments(documents); - when(documentVerificationRepository.setOtherDocumentSide("1", "2")).thenReturn(1); - verify(documentVerificationRepository, times(1)).setOtherDocumentSide("1", "2"); - when(documentVerificationRepository.setOtherDocumentSide("2", "1")).thenReturn(1); - verify(documentVerificationRepository, times(1)).setOtherDocumentSide("2", "1"); - assertEquals(docIdCardBack.getId(), docIdCardFront.getOtherSideId()); - assertEquals(docIdCardFront.getId(), docIdCardBack.getOtherSideId()); + void testResubmitDocuments_missingResubmitFlag() throws Exception { + final IdentityVerificationEntity identityVerification = identityVerificationRepository.findById("v1").get(); + assertNotNull(identityVerification); + + final List metadata = createIdCardMetadata(); + metadata.get(0).setOriginalDocumentId("original1"); + metadata.get(1).setOriginalDocumentId("original2"); + final List data = createIdCardData(); + final OwnerId ownerId = createOwnerId(); + + final DocumentSubmitRequest request = new DocumentSubmitRequest(); + request.setProcessId("p1"); + request.setResubmit(false); + request.setData("files".getBytes()); + request.setDocuments(metadata); + when(dataExtractionService.extractDocuments(request.getData())).thenReturn(data); + + final DocumentSubmitException exception = assertThrows(DocumentSubmitException.class, + () -> tested.submitDocuments(identityVerification, request, ownerId)); + assertEquals("Detected a submit request with specified originalDocumentId=original1, %s".formatted(ownerId), exception.getMessage()); + } + + private List createIdCardMetadata() { + final DocumentSubmitRequest.DocumentMetadata page1 = new DocumentSubmitRequest.DocumentMetadata(); + page1.setFilename("id_card_front.png"); + page1.setType(DocumentType.ID_CARD); + page1.setSide(CardSide.FRONT); + + final DocumentSubmitRequest.DocumentMetadata page2 = new DocumentSubmitRequest.DocumentMetadata(); + page2.setFilename("id_card_back.png"); + page2.setType(DocumentType.ID_CARD); + page2.setSide(CardSide.BACK); + + return List.of(page1, page2); + } + + private List createIdCardData() { + final Document documentPage1 = new Document(); + documentPage1.setData("img1".getBytes()); + documentPage1.setFilename("id_card_front.png"); + + final Document documentPage2 = new Document(); + documentPage2.setData("img2".getBytes()); + documentPage2.setFilename("id_card_back.png"); + + return List.of(documentPage1, documentPage2); + } + + private OwnerId createOwnerId() { + final OwnerId ownerId = new OwnerId(); + ownerId.setActivationId("a1"); + ownerId.setUserId("u1"); + return ownerId; } } diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.sql new file mode 100644 index 000000000..a8d5f54f3 --- /dev/null +++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.sql @@ -0,0 +1,2 @@ +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created) VALUES + ('v1', 'a1', 'u1', 'p1', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now()); diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testPairTwoSidedDocuments.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testPairTwoSidedDocuments.sql new file mode 100644 index 000000000..7f5ef45c6 --- /dev/null +++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testPairTwoSidedDocuments.sql @@ -0,0 +1,6 @@ +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created) VALUES + ('v1', 'a1', 'u1', 'p1', 'VERIFICATION_PENDING', 'DOCUMENT_VERIFICATION', now()); + +INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, side, status, used_for_verification, filename, timestamp_created) VALUES + ('1', 'a1', 'v1', 'ID_CARD', 'FRONT', 'VERIFICATION_PENDING', true, 'id_front.png', now()), + ('2', 'a1', 'v1', 'ID_CARD', 'BACK', 'VERIFICATION_PENDING', true, 'id_back.png', now()); diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testResubmitDocuments.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testResubmitDocuments.sql new file mode 100644 index 000000000..ecaf09aa7 --- /dev/null +++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingServiceTest.testResubmitDocuments.sql @@ -0,0 +1,6 @@ +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created) VALUES + ('v1', 'a1', 'u1', 'p1', 'VERIFICATION_PENDING', 'DOCUMENT_VERIFICATION', now()); + +INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, side, status, used_for_verification, filename, timestamp_created) VALUES + ('original1', 'a1', 'v1', 'ID_CARD', 'FRONT', 'VERIFICATION_PENDING', true, 'original_id_front.png', now()), + ('original2', 'a1', 'v1', 'ID_CARD', 'BACK', 'VERIFICATION_PENDING', true, 'original_id_back.png', now()); \ No newline at end of file From 893202236f406a0caf0b6c6785fd580afbb382ca Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Mon, 18 Dec 2023 10:47:22 +0100 Subject: [PATCH 41/52] Fix #962: Add Migration Guide for 1.6.0 --- docs/Migration-Instructions.md | 1 + docs/PowerAuth-Enrollment-Server-1.6.0.md | 5 +++++ docs/onboarding/Migration-Instructions.md | 1 + .../PowerAuth-Enrollment-Onboarding-Server-1.6.0.md | 5 +++++ 4 files changed, 12 insertions(+) create mode 100644 docs/PowerAuth-Enrollment-Server-1.6.0.md create mode 100644 docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md diff --git a/docs/Migration-Instructions.md b/docs/Migration-Instructions.md index 074891779..0bf91d4af 100644 --- a/docs/Migration-Instructions.md +++ b/docs/Migration-Instructions.md @@ -2,5 +2,6 @@ This page contains PowerAuth Enrollment Server migration instructions. +- [PowerAuth Enrollment Server 1.6.0](./PowerAuth-Enrollment-Server-1.6.0.md) - [PowerAuth Enrollment Server 1.5.0](./PowerAuth-Enrollment-Server-1.5.0.md) - [PowerAuth Enrollment Server 1.4.0](./PowerAuth-Enrollment-Server-1.4.0.md) diff --git a/docs/PowerAuth-Enrollment-Server-1.6.0.md b/docs/PowerAuth-Enrollment-Server-1.6.0.md new file mode 100644 index 000000000..66ea56171 --- /dev/null +++ b/docs/PowerAuth-Enrollment-Server-1.6.0.md @@ -0,0 +1,5 @@ +# Migration from 1.5.x to 1.6.x + +This guide contains instructions for migration from PowerAuth Enrollment Server version `1.5.x` to version `1.6.0`. + +No migration steps nor database changes are required. diff --git a/docs/onboarding/Migration-Instructions.md b/docs/onboarding/Migration-Instructions.md index 21b191aa9..9c502ee33 100644 --- a/docs/onboarding/Migration-Instructions.md +++ b/docs/onboarding/Migration-Instructions.md @@ -2,4 +2,5 @@ This page contains PowerAuth Enrollment Onboarding Server migration instructions. +- [PowerAuth Enrollment Onboarding Server 1.6.0](./PowerAuth-Enrollment-Onboarding-Server-1.6.0.md) - [PowerAuth Enrollment Onboarding Server 1.5.0](./PowerAuth-Enrollment-Onboarding-Server-1.5.0.md) diff --git a/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md b/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md new file mode 100644 index 000000000..1613bb122 --- /dev/null +++ b/docs/onboarding/PowerAuth-Enrollment-Onboarding-Server-1.6.0.md @@ -0,0 +1,5 @@ +# Migration from 1.5.x to 1.6.x + +This guide contains instructions for migration from PowerAuth Enrollment Onboarding Server version `1.5.x` to version `1.6.0`. + +No migration steps nor database changes are required. From 1ddfe6d974642c7a4e49c43793872834249c8d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pe=C5=A1ek?= Date: Mon, 18 Dec 2023 11:05:46 +0100 Subject: [PATCH 42/52] Fix #960: Remove code-level consistency check in Innovatrics Document Provider (#961) --- docs/onboarding/Configuration-Properties.md | 1 - .../innovatrics/InnovatricsConfigProps.java | 5 ----- ...novatricsDocumentVerificationProvider.java | 22 ++++--------------- .../resources/application-test.properties | 1 - .../src/main/resources/application.properties | 1 - 5 files changed, 4 insertions(+), 26 deletions(-) diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index 1918751fb..0ae467ce7 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -144,7 +144,6 @@ The Onboarding Server uses the following public configuration properties: | `enrollment-server-onboarding.provider.innovatrics.serviceUserAgent` | `Wultra/OnboardingServer` | User agent to use when making HTTP calls to Innovatrics REST service. | | `enrollment-server-onboarding.provider.innovatrics.presenceCheckConfiguration.score` | 0.875 | Presence check minimal score threshold. | | `enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries` | `CZE` | List of expected countries of issue of identification documents as three-letter country codes, i.e. ISO 3166-1 alpha-3. If empty, all countries of issue known to Innovatrics are considered during classification, which may have negative impact on performance. | -| `enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields` | `documentNumber`, `dateOfIssue`, `dateOfExpiry`, `surname`, `dateOfBirth`, `personalNumber`, `givenNames` | Set of fields in camelCase that are cross-validated between the machine-readable zone and visual zone. Only those specified fields that are actually extracted from a document are considered. If empty, no inconsistencies will be checked. | | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate` | `false` | Whether invalid SSL certificate is accepted when calling Zen ID REST service. | | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize` | `10485760` | Maximum in memory size of HTTP requests when calling Innovatrics REST service. | | `enrollment-server-onboarding.provider.innovatrics.restClientConfig.proxyEnabled` | `false` | Whether proxy server is enabled when calling Innovatrics REST service. | diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java index ce0acde45..da7adb429 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsConfigProps.java @@ -81,11 +81,6 @@ public static class DocumentVerificationConfiguration { * Identifies expected document countries of issue in ISO 3166-1 alpha-3 format. */ private List documentCountries; - - /** - * Set of fields in camelCase that are cross-validated between the machine-readable zone and visual zone, if extracted. - */ - private Set crucialFields; } } diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java index e3d134135..5f660561e 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsDocumentVerificationProvider.java @@ -372,37 +372,23 @@ private List parseVisualZoneInspection(final VisualZoneInspection visual final MrzConsistency mrzConsistency = textConsistentWith.getMrz(); if (mrzConsistency != null) { - final List inconsistentAttributes = getCrucial(mrzConsistency.getInconsistentTexts()); + final List inconsistentAttributes = mrzConsistency.getInconsistentTexts(); if (!inconsistentAttributes.isEmpty()) { - rejectionReasons.add("Inconsistent crucial attributes with MRZ: %s".formatted(inconsistentAttributes)); + rejectionReasons.add("Inconsistent attributes with MRZ: %s".formatted(inconsistentAttributes)); } } final BarcodesConsistency barcodesConsistency = textConsistentWith.getBarcodes(); if (barcodesConsistency != null) { - final List inconsistentAttributes = getCrucial(barcodesConsistency.getInconsistentTexts()); + final List inconsistentAttributes = barcodesConsistency.getInconsistentTexts(); if (!inconsistentAttributes.isEmpty()) { - rejectionReasons.add("Inconsistent crucial attributes with barcode: %s".formatted(inconsistentAttributes)); + rejectionReasons.add("Inconsistent attributes with barcode: %s".formatted(inconsistentAttributes)); } } return rejectionReasons; } - /** - * Intersects list of attributes with CRUCIAL_ATTRIBUTES. - * @param attributes Attributes to do the intersection on. - * @return Attributes intersection. - */ - private List getCrucial(final List attributes) { - if (attributes == null) { - return Collections.emptyList(); - } - - final Set crucialFields = configuration.getDocumentVerificationConfiguration().getCrucialFields(); - return attributes.stream().filter(crucialFields::contains).toList(); - } - private String serializeToString(T src) throws DocumentVerificationException { try { return objectMapper.writeValueAsString(src); diff --git a/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties index 8e68187cc..4c7f63087 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties +++ b/enrollment-server-onboarding-provider-innovatrics/src/test/resources/application-test.properties @@ -3,6 +3,5 @@ enrollment-server-onboarding.presence-check.provider=innovatrics enrollment-server-onboarding.onboarding-process.enabled=false enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE,SVK -enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames enrollment-server-onboarding.provider.innovatrics.restClientConfig.maxInMemorySize=1048576 \ No newline at end of file diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index 9b608a5d4..29f2fcf68 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -177,7 +177,6 @@ enrollment-server-onboarding.provider.innovatrics.presenceCheckConfiguration.sco # Innovatrics document-verification configuration enrollment-server-onboarding.provider.innovatrics.documentVerificationConfiguration.documentCountries=CZE -enrollment-server-onboarding.provider.innovatrics.document-verification-configuration.crucialFields=documentNumber, dateOfIssue, dateOfExpiry, surname, dateOfBirth, personalNumber, givenNames # Innovatrics REST client configuration enrollment-server-onboarding.provider.innovatrics.restClientConfig.acceptInvalidSslCertificate=false From 3a54da3c1c64bdb00d001e26f8aeb5d73f6acf49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Pe=C5=A1ek?= Date: Mon, 18 Dec 2023 11:15:41 +0100 Subject: [PATCH 43/52] Fix #956: Respect provider name in queries (#957) --- enrollment-server-onboarding-common/pom.xml | 6 ++ .../database/DocumentResultRepository.java | 6 +- ...ServerOnboardingCommonTestApplication.java | 24 +++++++ .../DocumentResultRepositoryTest.java | 65 +++++++++++++++++ .../resources/application-test.properties | 5 ++ ...lInProgressDocumentSubmitVerifications.sql | 12 ++++ ...testStreamAllInProgressDocumentSubmits.sql | 12 ++++ .../DocumentProcessingBatchService.java | 18 ++--- .../VerificationProcessingBatchService.java | 34 ++------- .../service/StateMachineService.java | 61 ++++++---------- .../service/PresenceCheckServiceTest.java | 11 --- .../service/StateMachineServiceTest.java | 70 +++++++++++++++++++ .../resources/application-test.properties | 2 + ...iceTest.testChangeMachineStatesInBatch.sql | 6 ++ ...ChangeMachineStatesInBatch_noDocuments.sql | 4 ++ ...tChangeMachineStatesInBatch_submitting.sql | 6 ++ 16 files changed, 249 insertions(+), 93 deletions(-) create mode 100644 enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java create mode 100644 enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java create mode 100644 enrollment-server-onboarding-common/src/test/resources/application-test.properties create mode 100644 enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql create mode 100644 enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql create mode 100644 enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql create mode 100644 enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql diff --git a/enrollment-server-onboarding-common/pom.xml b/enrollment-server-onboarding-common/pom.xml index 09f6359c5..d6d88b13e 100644 --- a/enrollment-server-onboarding-common/pom.xml +++ b/enrollment-server-onboarding-common/pom.xml @@ -61,6 +61,12 @@ spring-boot-starter-test test + + + com.h2database + h2 + test + diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java index cb0eab250..fdcc339ba 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepository.java @@ -40,18 +40,20 @@ public interface DocumentResultRepository extends CrudRepository streamAllInProgressDocumentSubmits(); + Stream streamAllInProgressDocumentSubmits(String providerName); /** * @return All not finished document submit verifications (upload is in progress and verification id exists) */ @Query("SELECT doc FROM DocumentResultEntity doc WHERE" + " doc.documentVerification.status = com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus.UPLOAD_IN_PROGRESS" + + " AND doc.documentVerification.providerName = :providerName " + " AND doc.documentVerification.verificationId IS NOT NULL" + " ORDER BY doc.timestampCreated ASC") - Stream streamAllInProgressDocumentSubmitVerifications(); + Stream streamAllInProgressDocumentSubmitVerifications(String providerName); /** * @return All document results for the specified document verification and processing phase diff --git a/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java new file mode 100644 index 000000000..437b51434 --- /dev/null +++ b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/EnrollmentServerOnboardingCommonTestApplication.java @@ -0,0 +1,24 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.common; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class EnrollmentServerOnboardingCommonTestApplication { +} diff --git a/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java new file mode 100644 index 000000000..c95706d24 --- /dev/null +++ b/enrollment-server-onboarding-common/src/test/java/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.java @@ -0,0 +1,65 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.common.database; + + +import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; +import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for {@link DocumentResultRepository}. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@DataJpaTest +@ActiveProfiles("test") +@Transactional +class DocumentResultRepositoryTest { + + @Autowired + private DocumentResultRepository tested; + + @Test + @Sql + void testStreamAllInProgressDocumentSubmits() { + assertThat(tested.streamAllInProgressDocumentSubmits("mock")) + .extracting(DocumentResultEntity::getDocumentVerification) + .extracting(DocumentVerificationEntity::getProviderName) + .containsOnly("mock") + .hasSize(1); + } + + @Test + @Sql + void testStreamAllInProgressDocumentSubmitVerifications() { + assertThat(tested.streamAllInProgressDocumentSubmitVerifications("mock")) + .extracting(DocumentResultEntity::getDocumentVerification) + .extracting(DocumentVerificationEntity::getProviderName) + .containsOnly("mock") + .hasSize(1); + } + +} diff --git a/enrollment-server-onboarding-common/src/test/resources/application-test.properties b/enrollment-server-onboarding-common/src/test/resources/application-test.properties new file mode 100644 index 000000000..308c2b3ac --- /dev/null +++ b/enrollment-server-onboarding-common/src/test/resources/application-test.properties @@ -0,0 +1,5 @@ +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.username=sa +spring.datasource.password=password +spring.datasource.driver-class-name=org.h2.Driver +spring.jpa.hibernate.ddl-auto=create diff --git a/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql new file mode 100644 index 000000000..a6affd76d --- /dev/null +++ b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmitVerifications.sql @@ -0,0 +1,12 @@ +-- Documents that have been already submitted and data were extracted (multiple providers). +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES +('v3', 'a3', 'u3', 'p3', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()), +('v4', 'a4', 'u4', 'p4', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()); + +INSERT INTO es_document_verification(id, provider_name, activation_id, identity_verification_id, verification_id, type, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES + ('d3', 'foreign', 'a3', 'v3', 'verification1', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f3', true, now(), now()), + ('d4', 'mock', 'a4', 'v4', 'verification2', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f4', true, now(), now()); + +INSERT INTO es_document_result(id, document_verification_id, phase, extracted_data, timestamp_created) VALUES + (3, 'd3', 'UPLOAD', '{extracted_data}', now()), + (4, 'd4', 'UPLOAD', '{extracted_data}', now()); diff --git a/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql new file mode 100644 index 000000000..e32e438eb --- /dev/null +++ b/enrollment-server-onboarding-common/src/test/resources/com/wultra/app/onboardingserver/common/database/DocumentResultRepositoryTest.testStreamAllInProgressDocumentSubmits.sql @@ -0,0 +1,12 @@ +-- Documents that have not been submitted yet (multiple providers). +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES + ('v1', 'a1', 'u1', 'p1', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()), + ('v2', 'a2', 'u2', 'p2', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()); + +INSERT INTO es_document_verification(id, provider_name, activation_id, identity_verification_id, type, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES + ('d1', 'foreign', 'a1', 'v1', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f1', true, now(), now()), + ('d2', 'mock', 'a2', 'v2', 'ID_CARD', 'UPLOAD_IN_PROGRESS', 'f2', true, now(), now()); + +INSERT INTO es_document_result(id, document_verification_id, phase, timestamp_created) VALUES + (1, 'd1', 'UPLOAD', now()), + (2, 'd2', 'UPLOAD', now()); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java index cb3cda261..2ffb2e34a 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/document/DocumentProcessingBatchService.java @@ -22,6 +22,8 @@ import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus; import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; +import lombok.AllArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +39,7 @@ * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ @Service +@AllArgsConstructor public class DocumentProcessingBatchService { private static final Logger logger = LoggerFactory.getLogger(DocumentProcessingBatchService.class); @@ -45,18 +48,7 @@ public class DocumentProcessingBatchService { private final DocumentProcessingService documentProcessingService; - /** - * Service constructor. - * @param documentResultRepository Document verification result repository. - * @param documentProcessingService Document processing service. - */ - @Autowired - public DocumentProcessingBatchService( - DocumentResultRepository documentResultRepository, - DocumentProcessingService documentProcessingService) { - this.documentResultRepository = documentResultRepository; - this.documentProcessingService = documentProcessingService; - } + private final IdentityVerificationConfig identityVerificationConfig; /** * Checks in progress document submits on current provider status and data result @@ -64,7 +56,7 @@ public DocumentProcessingBatchService( @Transactional public void checkInProgressDocumentSubmits() { AtomicInteger countFinished = new AtomicInteger(0); - try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmits()) { + try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmits(identityVerificationConfig.getDocumentVerificationProvider())) { stream.forEach(docResult -> { DocumentVerificationEntity docVerification = docResult.getDocumentVerification(); final OwnerId ownerId = new OwnerId(); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java index fb6282b5f..34672929b 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/verification/VerificationProcessingBatchService.java @@ -31,8 +31,10 @@ import com.wultra.app.onboardingserver.common.service.AuditService; import com.wultra.app.onboardingserver.common.service.CommonOnboardingService; import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; +import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService; import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; +import lombok.AllArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -49,6 +51,7 @@ * @author Lukas Lukovsky, lukas.lukovsky@wultra.com */ @Service +@AllArgsConstructor public class VerificationProcessingBatchService { private static final Logger logger = LoggerFactory.getLogger(VerificationProcessingBatchService.class); @@ -67,34 +70,7 @@ public class VerificationProcessingBatchService { private final CommonOnboardingService commonOnboardingService; - /** - * Service constructor. - * @param documentResultRepository Document verification result repository. - * @param documentVerificationProvider Document verification provider. - * @param identityVerificationRepository Identity verification repository. - * @param identityVerificationService Identity verification service. - * @param verificationProcessingService Verification processing service. - * @param auditService Audit service. - * @param commonOnboardingService Onboarding process service (common). - */ - @Autowired - public VerificationProcessingBatchService( - final DocumentResultRepository documentResultRepository, - final DocumentVerificationProvider documentVerificationProvider, - final IdentityVerificationRepository identityVerificationRepository, - final IdentityVerificationService identityVerificationService, - final VerificationProcessingService verificationProcessingService, - final AuditService auditService, - final CommonOnboardingService commonOnboardingService) { - - this.documentResultRepository = documentResultRepository; - this.identityVerificationRepository = identityVerificationRepository; - this.documentVerificationProvider = documentVerificationProvider; - this.identityVerificationService = identityVerificationService; - this.verificationProcessingService = verificationProcessingService; - this.auditService = auditService; - this.commonOnboardingService = commonOnboardingService; - } + private final IdentityVerificationConfig identityVerificationConfig; /** * Checks document submit verifications @@ -102,7 +78,7 @@ public VerificationProcessingBatchService( @Transactional public void checkDocumentSubmitVerifications() { AtomicInteger countFinished = new AtomicInteger(0); - try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmitVerifications()) { + try (Stream stream = documentResultRepository.streamAllInProgressDocumentSubmitVerifications(identityVerificationConfig.getDocumentVerificationProvider())) { stream.forEach(docResult -> { DocumentVerificationEntity docVerification = docResult.getDocumentVerification(); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java index b871dc4fe..5b1357537 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineService.java @@ -19,6 +19,7 @@ import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.errorhandling.IdentityVerificationException; +import com.wultra.app.onboardingserver.configuration.IdentityVerificationConfig; import com.wultra.app.onboardingserver.impl.service.IdentityVerificationService; import com.wultra.app.onboardingserver.statemachine.EnrollmentStateProvider; import com.wultra.app.onboardingserver.statemachine.consts.EventHeaderName; @@ -27,6 +28,7 @@ import com.wultra.app.onboardingserver.statemachine.enums.OnboardingState; import com.wultra.app.onboardingserver.statemachine.interceptor.CustomStateMachineInterceptor; import jakarta.annotation.Nullable; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.messaging.Message; @@ -53,6 +55,7 @@ */ @Service @Slf4j +@AllArgsConstructor @ConditionalOnProperty(value = "enrollment-server-onboarding.identity-verification.enabled", havingValue = "true") public class StateMachineService { @@ -66,27 +69,7 @@ public class StateMachineService { private final TransactionTemplate transactionTemplate; - /** - * Constructor. - * - * @param enrollmentStateProvider Enrollment state provider. - * @param stateMachineFactory State machine factory. - * @param stateMachineInterceptor State machine interceptor. - * @param identityVerificationService Identity verification service. - */ - public StateMachineService( - final EnrollmentStateProvider enrollmentStateProvider, - final StateMachineFactory stateMachineFactory, - final CustomStateMachineInterceptor stateMachineInterceptor, - final IdentityVerificationService identityVerificationService, - final TransactionTemplate transactionTemplate - ) { - this.enrollmentStateProvider = enrollmentStateProvider; - this.stateMachineFactory = stateMachineFactory; - this.stateMachineInterceptor = stateMachineInterceptor; - this.identityVerificationService = identityVerificationService; - this.transactionTemplate = transactionTemplate; - } + private final IdentityVerificationConfig identityVerificationConfig; @Transactional public StateMachine processStateMachineEvent(OwnerId ownerId, String processId, OnboardingEvent event) @@ -144,23 +127,25 @@ public Message createMessage(OwnerId ownerId, String processId, public void changeMachineStatesInBatch() { final AtomicInteger countFinished = new AtomicInteger(0); try (Stream stream = identityVerificationService.streamAllIdentityVerificationsToChangeState().parallel()) { - stream.forEach(identityVerification -> { - final String processId = identityVerification.getProcessId(); - final OwnerId ownerId = new OwnerId(); - ownerId.setActivationId(identityVerification.getActivationId()); - ownerId.setUserId(identityVerification.getUserId()); - logger.debug("Changing state of machine for process ID: {}", processId); - - transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); - transactionTemplate.executeWithoutResult(status -> { - try { - processStateMachineEvent(ownerId, processId, OnboardingEvent.EVENT_NEXT_STATE); - countFinished.incrementAndGet(); - } catch (IdentityVerificationException e) { - logger.warn("Unable to change state for process ID: {}", processId, e); - } - }); - }); + stream.filter(identityVerification -> identityVerification.getDocumentVerifications().stream() + .anyMatch(doc -> identityVerificationConfig.getDocumentVerificationProvider().equals(doc.getProviderName()))) + .forEach(identityVerification -> { + final String processId = identityVerification.getProcessId(); + final OwnerId ownerId = new OwnerId(); + ownerId.setActivationId(identityVerification.getActivationId()); + ownerId.setUserId(identityVerification.getUserId()); + logger.debug("Changing state of machine for process ID: {}", processId); + + transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + transactionTemplate.executeWithoutResult(status -> { + try { + processStateMachineEvent(ownerId, processId, OnboardingEvent.EVENT_NEXT_STATE); + countFinished.incrementAndGet(); + } catch (IdentityVerificationException e) { + logger.warn("Unable to change state for process ID: {}", processId, e); + } + }); + }); } if (countFinished.get() > 0) { logger.debug("Changed state of {} identity verifications", countFinished.get()); diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java index c256c6d5d..93ea22fbe 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/PresenceCheckServiceTest.java @@ -18,34 +18,23 @@ import com.wultra.app.enrollmentserver.model.enumeration.CardSide; import com.wultra.app.enrollmentserver.model.enumeration.DocumentType; -import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase; -import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus; import com.wultra.app.enrollmentserver.model.integration.Image; import com.wultra.app.enrollmentserver.model.integration.OwnerId; import com.wultra.app.enrollmentserver.model.integration.SessionInfo; import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; import com.wultra.app.onboardingserver.api.provider.PresenceCheckProvider; import com.wultra.app.onboardingserver.common.database.DocumentVerificationRepository; -import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository; -import com.wultra.app.onboardingserver.common.database.ScaResultRepository; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; -import com.wultra.app.onboardingserver.impl.service.internal.JsonSerializationService; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import java.util.List; import java.util.Optional; -import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.DOCUMENT_VERIFICATION_FINAL; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.PRESENCE_CHECK; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.*; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java new file mode 100644 index 000000000..9972b40bd --- /dev/null +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.java @@ -0,0 +1,70 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.onboardingserver.statemachine.service; + +import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus; +import com.wultra.app.onboardingserver.EnrollmentServerTestApplication; +import com.wultra.app.onboardingserver.common.database.IdentityVerificationRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test for {@link StateMachineService} + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +@SpringBootTest(classes = EnrollmentServerTestApplication.class) +@ActiveProfiles("test") +class StateMachineServiceTest { + + @Autowired + private StateMachineService tested; + + @Autowired + private IdentityVerificationRepository repository; + + @Test + @Sql + void testChangeMachineStatesInBatch() { + tested.changeMachineStatesInBatch(); + + assertEquals(IdentityVerificationStatus.VERIFICATION_PENDING, repository.findById("v1").get().getStatus()); + } + + @Test + @Sql + void testChangeMachineStatesInBatch_submitting() { + tested.changeMachineStatesInBatch(); + + assertEquals(IdentityVerificationStatus.IN_PROGRESS, repository.findById("v2").get().getStatus()); + } + + @Test + @Sql + void testChangeMachineStatesInBatch_noDocuments() { + tested.changeMachineStatesInBatch(); + + assertEquals(IdentityVerificationStatus.IN_PROGRESS, repository.findById("v3").get().getStatus()); + } + +} diff --git a/enrollment-server-onboarding/src/test/resources/application-test.properties b/enrollment-server-onboarding/src/test/resources/application-test.properties index 50a2b529f..5dff6a04e 100644 --- a/enrollment-server-onboarding/src/test/resources/application-test.properties +++ b/enrollment-server-onboarding/src/test/resources/application-test.properties @@ -24,3 +24,5 @@ spring.jpa.hibernate.ddl-auto=create spring.liquibase.enabled=false enrollment-server-onboarding.identity-verification.enabled=true +enrollment-server-onboarding.document-verification.provider=mock + diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql new file mode 100644 index 000000000..740d005e4 --- /dev/null +++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch.sql @@ -0,0 +1,6 @@ +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES + ('v1', 'a1', 'u1', 'p1', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()); + +-- document already submitted to 'mock' provider +INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, provider_name, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES + ('doc1', 'a1', 'v1', 'ID_CARD', 'mock', 'VERIFICATION_PENDING', 'f2', true, now(), now()); diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql new file mode 100644 index 000000000..fb63b4bdf --- /dev/null +++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_noDocuments.sql @@ -0,0 +1,4 @@ +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES + ('v3', 'a3', 'u3', 'p3', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()); + +-- no documents submitted yet diff --git a/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql new file mode 100644 index 000000000..5cf138318 --- /dev/null +++ b/enrollment-server-onboarding/src/test/resources/com/wultra/app/onboardingserver/statemachine/service/StateMachineServiceTest.testChangeMachineStatesInBatch_submitting.sql @@ -0,0 +1,6 @@ +INSERT INTO es_identity_verification(id, activation_id, user_id, process_id, status, phase, timestamp_created, timestamp_last_updated) VALUES + ('v2', 'a2', 'u2', 'p2', 'IN_PROGRESS', 'DOCUMENT_UPLOAD', now(), now()); + +-- document is being submitted to a provider +INSERT INTO es_document_verification(id, activation_id, identity_verification_id, type, provider_name, status, filename, used_for_verification, timestamp_created, timestamp_last_updated) VALUES + ('doc2', 'a2', 'v2', 'ID_CARD', null, 'UPLOAD_IN_PROGRESS', 'f1', true, now(), now()); From dd7cf0a4083332f6e07e9e60fe0825bfb4d8d22d Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 21 Dec 2023 07:19:14 +0100 Subject: [PATCH 44/52] Fix #968: MonetaryConverter throws an exception for currency '...' --- .../impl/service/converter/MonetaryConverter.java | 4 ++-- .../impl/service/converter/MonetaryConverterTest.java | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java index 35022cb83..3b9321ba2 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverter.java @@ -21,7 +21,7 @@ import javax.money.CurrencyUnit; import javax.money.Monetary; -import javax.money.UnknownCurrencyException; +import javax.money.MonetaryException; import java.math.RoundingMode; import java.text.NumberFormat; import java.util.Currency; @@ -113,7 +113,7 @@ private static int getFractionDigits(String code) { try { final CurrencyUnit currencyUnit = Monetary.getCurrency(code); return currencyUnit.getDefaultFractionDigits(); - } catch (UnknownCurrencyException e) { + } catch (MonetaryException e) { logger.debug("No currency mapping for code={}, most probably not FIAT", code); logger.trace("No currency mapping for code={}", code, e); return DEFAULT_MINIMAL_FRACTION_DIGITS; diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java index 4760d9bec..38e245b9d 100644 --- a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java +++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MonetaryConverterTest.java @@ -47,7 +47,8 @@ class MonetaryConverterTest { "NZD, en, NZ$", "NZD, cs, NZ$", "NZD, nz, NZ$", - "BTC, cs, BTC" + "BTC, cs, BTC", + "..., cs, ..." }) void testFormatCurrency(final String source, final String locale, final String expected) { final String result = MonetaryConverter.formatCurrency(source, new Locale(locale)); @@ -68,7 +69,8 @@ void testFormatCurrency(final String source, final String locale, final String e "1, BTC, en, '1'", "1.1, BTC, en, '1.1'", "0.123456789, BTC, en, '0.123456789'", - "0.567567567567567567567, BTC, en, '0.567567567567567567'" + "0.567567567567567567567, BTC, en, '0.567567567567567567'", + "1, ..., en, '1'" }) void testFormatAmount(final String amount, final String code, final String locale, final String expected) { final String result = MonetaryConverter.formatAmount(new BigDecimal(amount), code, new Locale(locale)); @@ -92,7 +94,8 @@ void testFormatAmount(final String amount, final String code, final String local "1, BTC, en, '1 BTC'", "1.1, BTC, en, '1.1 BTC'", "0.123456789, BTC, en, '0.123456789 BTC'", - "0.567567567567567567567, BTC, en, '0.567567567567567567 BTC'" + "0.567567567567567567567, BTC, en, '0.567567567567567567 BTC'", + "1, ..., en, '1 ...'" }) void testFormatValue(final String amount, final String code, final String locale, final String expected) { final String result = MonetaryConverter.formatValue(new BigDecimal(amount), code, new Locale(locale)); From 059a9269f5c9a5a8f0d64a7d677304acd9d7739f Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Thu, 21 Dec 2023 08:51:07 +0100 Subject: [PATCH 45/52] Fix #966: Add micrometer-registry-prometheus --- docs/Configuration-Properties.md | 6 ++++++ docs/onboarding/Configuration-Properties.md | 6 ++++++ enrollment-server-onboarding/pom.xml | 6 ++++++ .../src/main/resources/application.properties | 8 +++++++- enrollment-server/pom.xml | 6 ++++++ .../src/main/resources/application.properties | 8 +++++++- 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/docs/Configuration-Properties.md b/docs/Configuration-Properties.md index 9a0295a8d..46718a263 100644 --- a/docs/Configuration-Properties.md +++ b/docs/Configuration-Properties.md @@ -60,3 +60,9 @@ Sample setting of logging pattern: ```properties logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint}%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx} ``` + + +## Monitoring and Observability + +The WAR file includes the `micrometer-registry-prometheus` dependency. +Discuss its configuration with the [Spring Boot documentation](https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/actuator.html#actuator.metrics). diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index 0ae467ce7..799d45a15 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -167,3 +167,9 @@ Sample setting of logging pattern: ```properties logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint}%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx} ``` + + +## Monitoring and Observability + +The WAR file includes the `micrometer-registry-prometheus` dependency. +Discuss its configuration with the [Spring Boot documentation](https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/actuator.html#actuator.metrics). diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index 10ba0fb81..3c9f79bb3 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -142,6 +142,12 @@ logstash-logback-encoder + + + io.micrometer + micrometer-registry-prometheus + + org.springframework.boot diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index 29f2fcf68..09f3c82f0 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -206,4 +206,10 @@ powerauth.service.correlation-header.enabled=false powerauth.service.correlation-header.name=X-Correlation-ID powerauth.service.correlation-header.value.validation-regexp=[a-zA-Z0-9\\-]{8,1024} # For logging correlation HTTP headers enable the pattern and update correlation header name in the pattern -#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx} \ No newline at end of file +#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx} + +# Monitoring +#management.endpoint.metrics.enabled=true +#management.endpoints.web.exposure.include=health, prometheus +#management.endpoint.prometheus.enabled=true +#management.prometheus.metrics.export.enabled=true diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index 1da830ea3..bd0e7e310 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -117,6 +117,12 @@ logstash-logback-encoder + + + io.micrometer + micrometer-registry-prometheus + + org.springframework.boot diff --git a/enrollment-server/src/main/resources/application.properties b/enrollment-server/src/main/resources/application.properties index c16c6f2b3..a0da944e3 100644 --- a/enrollment-server/src/main/resources/application.properties +++ b/enrollment-server/src/main/resources/application.properties @@ -87,4 +87,10 @@ powerauth.service.correlation-header.enabled=false powerauth.service.correlation-header.name=X-Correlation-ID powerauth.service.correlation-header.value.validation-regexp=[a-zA-Z0-9\\-]{8,1024} # For logging correlation HTTP headers enable the pattern and update correlation header name in the pattern -#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx} \ No newline at end of file +#logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) [%X{X-Correlation-ID}] %clr(%5p) %clr(${PID: }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx} + +# Monitoring +#management.endpoint.metrics.enabled=true +#management.endpoints.web.exposure.include=health, prometheus +#management.endpoint.prometheus.enabled=true +#management.prometheus.metrics.export.enabled=true From ed83f97f1248626c8ad0b27aef073588cd6a267b Mon Sep 17 00:00:00 2001 From: Jan Dusil <134381434+jandusil@users.noreply.github.com> Date: Thu, 21 Dec 2023 17:08:15 +0100 Subject: [PATCH 46/52] Fix #965: Reflect changes in filtering operations on PowerAuth Server (#967) * Fix #965: Reflect changes in filtering operations on PowerAuth Server - Change activationFlags to activationId for filtering of requested operations - Add test for MobileTokenService --- .../controller/api/MobileTokenController.java | 8 +- .../impl/service/MobileTokenService.java | 40 ++-- .../impl/service/MobileTokenServiceTest.java | 203 ++++++++++++++++++ 3 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java index 3da69aba1..1ef71aeb8 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/MobileTokenController.java @@ -106,9 +106,9 @@ public ObjectResponse operationList(@Parameter(hidden = t if (auth != null) { final String userId = auth.getUserId(); final String applicationId = auth.getApplicationId(); - final List activationFlags = auth.getActivationContext().getActivationFlags(); + final String activationId = auth.getActivationContext().getActivationId(); final String language = locale.getLanguage(); - final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationFlags, true); + final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationId, true); final Date currentTimestamp = new Date(); return new MobileTokenResponse<>(listResponse, currentTimestamp); } else { @@ -211,9 +211,9 @@ public ObjectResponse operationListAll(@Parameter(hidden if (auth != null) { final String userId = auth.getUserId(); final String applicationId = auth.getApplicationId(); - final List activationFlags = auth.getActivationContext().getActivationFlags(); + final String activationId = auth.getActivationContext().getActivationId(); final String language = locale.getLanguage(); - final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationFlags, false); + final OperationListResponse listResponse = mobileTokenService.operationListForUser(userId, applicationId, language, activationId, false); return new ObjectResponse<>(listResponse); } else { throw new MobileTokenAuthException(); diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java index 5f27018c6..9b26f27cc 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenService.java @@ -87,23 +87,25 @@ public MobileTokenService(PowerAuthClient powerAuthClient, MobileTokenConverter } /** - * Get the operation list with operations of a given users. The service either returns only pending - * operations or all operations, depending on the provided flag. + * Retrieves a list of operations for a specified user. This method can return + * either all operations or only those that are pending, based on the 'pendingOnly' flag. + * It processes each operation detail, converts them into a consistent format, and + * filters out operations without a corresponding template. * - * @param userId User ID. - * @param applicationId Application ID. - * @param language Language. - * @param activationFlags Activation flags to condition the operation against. - * @param pendingOnly Flag indicating if only pending or all operation should be returned. - * @return Response with pending or all operations, depending on the "pendingOnly" flag. - * @throws PowerAuthClientException In the case that PowerAuth service call fails. - * @throws MobileTokenConfigurationException In the case of system misconfiguration. + * @param userId User ID for which the operation list is requested. + * @param applicationId Application ID associated with the operations. + * @param language Language for operation template localization. + * @param activationId Optional activation ID to filter operations. + * @param pendingOnly Flag indicating whether to fetch only pending operations or all. + * @return A consolidated list of operations formatted as 'OperationListResponse'. + * @throws PowerAuthClientException If there's an issue with the PowerAuth service call. + * @throws MobileTokenConfigurationException For any system configuration issues. */ public OperationListResponse operationListForUser( @NotNull String userId, @NotNull String applicationId, @NotNull String language, - List activationFlags, + String activationId, boolean pendingOnly) throws PowerAuthClientException, MobileTokenConfigurationException { final OperationListForUserRequest request = new OperationListForUserRequest(); @@ -111,6 +113,7 @@ public OperationListResponse operationListForUser( request.setApplications(List.of(applicationId)); request.setPageNumber(0); request.setPageSize(OPERATION_LIST_LIMIT); + request.setActivationId(activationId); final MultiValueMap queryParams = httpCustomizationService.getQueryParams(); final MultiValueMap httpHeaders = httpCustomizationService.getHttpHeaders(); final com.wultra.security.powerauth.client.model.response.OperationListResponse operations = @@ -120,16 +123,13 @@ public OperationListResponse operationListForUser( final OperationListResponse responseObject = new OperationListResponse(); for (OperationDetailResponse operationDetail: operations) { - final String activationFlag = operationDetail.getActivationFlag(); - if (activationFlag == null || activationFlags.contains(activationFlag)) { // only return data if there is no flag, or if flag matches flags of activation - final Optional operationTemplate = operationTemplateService.findTemplate(operationDetail.getOperationType(), language); - if (operationTemplate.isEmpty()) { - logger.warn("No template found for operationType={}, skipping the entry.", operationDetail.getOperationType()); - continue; - } - final Operation operation = mobileTokenConverter.convert(operationDetail, operationTemplate.get()); - responseObject.add(operation); + final Optional operationTemplate = operationTemplateService.findTemplate(operationDetail.getOperationType(), language); + if (operationTemplate.isEmpty()) { + logger.warn("No template found for operationType={}, skipping the entry.", operationDetail.getOperationType()); + continue; } + final Operation operation = mobileTokenConverter.convert(operationDetail, operationTemplate.get()); + responseObject.add(operation); } return responseObject; } diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java new file mode 100644 index 000000000..e125f7d38 --- /dev/null +++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/MobileTokenServiceTest.java @@ -0,0 +1,203 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2023 Wultra s.r.o. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.wultra.app.enrollmentserver.impl.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.wultra.app.enrollmentserver.database.OperationTemplateRepository; +import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity; +import com.wultra.app.enrollmentserver.impl.service.converter.MobileTokenConverter; +import com.wultra.security.powerauth.client.PowerAuthClient; +import com.wultra.security.powerauth.client.model.request.OperationListForUserRequest; +import com.wultra.security.powerauth.client.model.response.OperationDetailResponse; +import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation; +import com.wultra.security.powerauth.lib.mtoken.model.response.OperationListResponse; +import io.getlime.security.powerauth.rest.api.spring.service.HttpCustomizationService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.context.ActiveProfiles; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +/** + * Test for {@link MobileTokenService}. + * + * @author Jan Dusil, jan.dusil@wultra.com + */ +@ExtendWith(MockitoExtension.class) +@ActiveProfiles("test") +class MobileTokenServiceTest { + + @Mock + private PowerAuthClient powerAuthClient; + + @Mock + private MobileTokenConverter mobileTokenConverter; + + @Mock + private OperationTemplateService operationTemplateService; + + @Mock + private HttpCustomizationService httpCustomizationService; + + @InjectMocks + private MobileTokenService tested; + + @Mock + private OperationTemplateRepository templateRepository; + + private static final int OPERATION_LIST_LIMIT = 100; + + @Test + void testOperationListForUser() throws Exception { + final String userId = "test-user"; + final String applicationId = "21"; + final String language = "CZ"; + final String activationId = "test-activation"; + final String operationType = "login"; + + final OperationListForUserRequest request = new OperationListForUserRequest(); + request.setUserId(userId); + request.setApplications(List.of(applicationId)); + request.setPageNumber(0); + request.setPageSize(OPERATION_LIST_LIMIT); + request.setActivationId(activationId); + + final OperationDetailResponse operationDetailResponse = new OperationDetailResponse(); + operationDetailResponse.setUserId(userId); + operationDetailResponse.setApplications(List.of(applicationId)); + operationDetailResponse.setOperationType(operationType); + operationDetailResponse.setParameters(new HashMap<>()); + + final com.wultra.security.powerauth.client.model.response.OperationListResponse response + = new com.wultra.security.powerauth.client.model.response.OperationListResponse(); + response.add(operationDetailResponse); + + when(powerAuthClient.operationList(request, null, null)).thenReturn(response); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + operationTemplate.setLanguage(language); + operationTemplate.setPlaceholder(operationType); + operationTemplate.setId(1L); + when(operationTemplateService.findTemplate(operationType, language)).thenReturn(Optional.of(operationTemplate)); + + final Operation operation = new Operation(); + operation.setName(operationType); + when(mobileTokenConverter.convert(operationDetailResponse, operationTemplate)).thenReturn(operation); + + final OperationListResponse operationListResponse = tested.operationListForUser(userId, applicationId, language, activationId, false); + + assertNotNull(operationListResponse); + assertEquals(1, operationListResponse.size()); + assertNotNull(operationListResponse.get(0)); + assertEquals(operationType, operationListResponse.get(0).getName()); + } + + @Test + void testPendingOperationListForUser() throws Exception { + final String userId = "test-user"; + final String applicationId = "21"; + final String language = "CZ"; + final String activationId = "test-activation"; + final String operationType = "login"; + + final OperationListForUserRequest request = new OperationListForUserRequest(); + request.setUserId(userId); + request.setApplications(List.of(applicationId)); + request.setPageNumber(0); + request.setPageSize(OPERATION_LIST_LIMIT); + request.setActivationId(activationId); + + final OperationDetailResponse operationDetailResponse = new OperationDetailResponse(); + operationDetailResponse.setUserId(userId); + operationDetailResponse.setApplications(List.of(applicationId)); + operationDetailResponse.setOperationType(operationType); + operationDetailResponse.setParameters(new HashMap<>()); + + final com.wultra.security.powerauth.client.model.response.OperationListResponse response + = new com.wultra.security.powerauth.client.model.response.OperationListResponse(); + response.add(operationDetailResponse); + + when(powerAuthClient.operationPendingList(request, null, null)).thenReturn(response); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + operationTemplate.setLanguage(language); + operationTemplate.setPlaceholder(operationType); + operationTemplate.setId(1L); + when(operationTemplateService.findTemplate(operationType, language)).thenReturn(Optional.of(operationTemplate)); + + final Operation operation = new Operation(); + operation.setName(operationType); + when(mobileTokenConverter.convert(operationDetailResponse, operationTemplate)).thenReturn(operation); + + final OperationListResponse operationListResponse = tested.operationListForUser(userId, applicationId, language, activationId, true); + + assertNotNull(operationListResponse); + assertEquals(1, operationListResponse.size()); + assertNotNull(operationListResponse.get(0)); + assertEquals(operationType, operationListResponse.get(0).getName()); + } + + @Test + void testOperationListForUserEmptyOperationTemplate() throws Exception { + final String userId = "test-user"; + final String applicationId = "21"; + final String language = "CZ"; + final String activationId = "test-activation"; + final String operationType = "login"; + + final OperationListForUserRequest request = new OperationListForUserRequest(); + request.setUserId(userId); + request.setApplications(List.of(applicationId)); + request.setPageNumber(0); + request.setPageSize(OPERATION_LIST_LIMIT); + request.setActivationId(activationId); + + final OperationDetailResponse operationDetailResponse = new OperationDetailResponse(); + operationDetailResponse.setUserId(userId); + operationDetailResponse.setApplications(List.of(applicationId)); + operationDetailResponse.setOperationType(operationType); + operationDetailResponse.setParameters(new HashMap<>()); + + final com.wultra.security.powerauth.client.model.response.OperationListResponse response + = new com.wultra.security.powerauth.client.model.response.OperationListResponse(); + response.add(operationDetailResponse); + + when(powerAuthClient.operationList(request, null, null)).thenReturn(response); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + operationTemplate.setLanguage(language); + operationTemplate.setPlaceholder(operationType); + operationTemplate.setId(1L); + + when(operationTemplateService.findTemplate(operationType, language)).thenReturn(Optional.empty()); + + + final OperationListResponse operationListResponse = tested.operationListForUser(userId, applicationId, language, activationId, false); + + assertNotNull(operationListResponse); + assertEquals(0, operationListResponse.size()); + } + +} From df26995a87900e293f40e6f5eac9747b2bb49c1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 02:50:51 +0000 Subject: [PATCH 47/52] Bump org.openapitools:openapi-generator-maven-plugin from 7.1.0 to 7.2.0 Bumps org.openapitools:openapi-generator-maven-plugin from 7.1.0 to 7.2.0. --- updated-dependencies: - dependency-name: org.openapitools:openapi-generator-maven-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6cd680b3d..79304f96b 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ - 7.1.0 + 7.2.0 5.10.2 4.0.0 From b11f7ab6628ceb4273fbb0b62353909cdd05ee5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Dec 2023 02:51:46 +0000 Subject: [PATCH 48/52] Bump io.swagger.core.v3:swagger-annotations-jakarta Bumps io.swagger.core.v3:swagger-annotations-jakarta from 2.2.19 to 2.2.20. --- updated-dependencies: - dependency-name: io.swagger.core.v3:swagger-annotations-jakarta dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6cd680b3d..169923961 100644 --- a/pom.xml +++ b/pom.xml @@ -92,7 +92,7 @@ 5.10.2 4.0.0 - 2.2.19 + 2.2.20 2.3.0 1.4.2 From 4b58e1aa9405e26982ecfd1f0c7462c40d33adb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lubo=C5=A1=20Ra=C4=8Dansk=C3=BD?= Date: Tue, 2 Jan 2024 13:45:29 +0100 Subject: [PATCH 49/52] Fix #970: AmountAttribute with invalid amount (#971) * Fix #970: AmountAttribute with invalid amount --- .../converter/MobileTokenConverter.java | 70 ++++----- .../converter/MobileTokenConverterTest.java | 144 ++++++++++++++++++ 2 files changed, 180 insertions(+), 34 deletions(-) diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java index 27baae4d7..2bd4b8cf7 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverter.java @@ -285,29 +285,39 @@ private static Optional buildAmountAttribute(final OperationTemplateP if (currency.isEmpty()) { return Optional.empty(); } - final BigDecimal amountRaw; - try { - amountRaw = new BigDecimal(amount.get()); - } catch (NumberFormatException ex) { - logger.warn("Invalid number format: {}, skipping the AMOUNT attribute!", amount); - return Optional.empty(); - } + final Locale locale = LocaleContextHolder.getLocale(); final String currencyRaw = currency.get(); final String currencyFormatted = MonetaryConverter.formatCurrency(currencyRaw, locale); - final String amountFormatted = MonetaryConverter.formatAmount(amountRaw, currencyRaw, locale); - final String valueFormatted = MonetaryConverter.formatValue(amountRaw, currencyRaw, locale); + final AmountFormatted amountFormatted = createAmountFormatted(amount.get(), currencyRaw, "AMOUNT"); return Optional.of(AmountAttribute.builder() .id(id) .label(text) - .amount(amountRaw) - .amountFormatted(amountFormatted) + .amount(amountFormatted.amountRaw()) + .amountFormatted(amountFormatted.amountFormatted()) .currency(currencyRaw) .currencyFormatted(currencyFormatted) - .valueFormatted(valueFormatted) + .valueFormatted(amountFormatted.valueFormatted()) .build()); } + private static AmountFormatted createAmountFormatted(final String amount, final String currencyRaw, final String attribute) { + final Locale locale = LocaleContextHolder.getLocale(); + + try { + final BigDecimal amountRaw = new BigDecimal(amount); + final String amountFormatted = MonetaryConverter.formatAmount(amountRaw, currencyRaw, locale); + final String valueFormatted = MonetaryConverter.formatValue(amountRaw, currencyRaw, locale); + return new AmountFormatted(amountRaw, amountFormatted, valueFormatted); + } catch (NumberFormatException e) { + logger.warn("Invalid number format: {}, the raw value is not filled in into {} attribute!", amount, attribute); + logger.trace("Invalid number format: {}, the raw value is not filled in into {} attribute!", amount, attribute, e); + // fallback - pass 'not a number' directly to the formatted field + final String valueFormatted = amount + " " + currencyRaw; + return new AmountFormatted(null, amount, valueFormatted); + } + } + private static Optional buildAmountConversionAttribute(final OperationTemplateParam templateParam, final Map params) { final String id = templateParam.getId(); final String text = templateParam.getText(); @@ -326,39 +336,29 @@ private static Optional buildAmountConversionAttribute(final Operatio .map(Boolean::parseBoolean) .orElse(false); - final BigDecimal sourceAmountRaw; - final BigDecimal targetAmountRaw; - try { - sourceAmountRaw = new BigDecimal(sourceAmount.get()); - targetAmountRaw = new BigDecimal(targetAmount.get()); - } catch (NumberFormatException ex) { - logger.warn("Invalid number format: {}, skipping the AMOUNT_CONVERSION attribute!", sourceAmount); - return Optional.empty(); - } - final Locale locale = LocaleContextHolder.getLocale(); final String sourceCurrencyRaw = sourceCurrency.get(); - final String sourceCurrencyFormatted = MonetaryConverter.formatCurrency(sourceCurrencyRaw, locale); - final String sourceAmountFormatted = MonetaryConverter.formatAmount(sourceAmountRaw, sourceCurrencyRaw, locale); - final String sourceValueFormatted = MonetaryConverter.formatValue(sourceAmountRaw, sourceCurrencyRaw, locale); + final AmountFormatted sourceAmountFormatted = createAmountFormatted(sourceAmount.get(), sourceCurrencyRaw, "AMOUNT_CONVERSION"); + final String targetCurrencyRaw = targetCurrency.get(); + final AmountFormatted targetAmountFormatted = createAmountFormatted(targetAmount.get(), targetCurrencyRaw, "AMOUNT_CONVERSION"); + + final String sourceCurrencyFormatted = MonetaryConverter.formatCurrency(sourceCurrencyRaw, locale); final String targetCurrencyFormatted = MonetaryConverter.formatCurrency(targetCurrencyRaw, locale); - final String targetAmountFormatted = MonetaryConverter.formatAmount(targetAmountRaw, targetCurrencyRaw, locale); - final String targetValueFormatted = MonetaryConverter.formatValue(targetAmountRaw, targetCurrencyRaw, locale); return Optional.of(AmountConversionAttribute.builder() .id(id) .label(text) .dynamic(dynamic) - .sourceAmount(sourceAmountRaw) - .sourceAmountFormatted(sourceAmountFormatted) + .sourceAmount(sourceAmountFormatted.amountRaw()) + .sourceAmountFormatted(sourceAmountFormatted.amountFormatted()) .sourceCurrency(sourceCurrencyRaw) .sourceCurrencyFormatted(sourceCurrencyFormatted) - .sourceValueFormatted(sourceValueFormatted) - .targetAmount(targetAmountRaw) - .targetAmountFormatted(targetAmountFormatted) + .sourceValueFormatted(sourceAmountFormatted.valueFormatted()) + .targetAmount(targetAmountFormatted.amountRaw()) + .targetAmountFormatted(targetAmountFormatted.amountFormatted()) .targetCurrency(targetCurrencyRaw) .targetCurrencyFormatted(targetCurrencyFormatted) - .targetValueFormatted(targetValueFormatted) + .targetValueFormatted(targetAmountFormatted.valueFormatted()) .build()); } @@ -397,7 +397,7 @@ private static Optional fetchTemplateParamValue(final OperationTemplateP return Optional.empty(); } if (params == null) { - logger.warn("Params of OperationDetailResponse is null"); + logger.warn("Params of OperationTemplateParam is null"); return Optional.empty(); } return Optional.ofNullable(templateParams.get(key)) @@ -408,4 +408,6 @@ private static String fetchTemplateParamValueNullable(final OperationTemplatePar return fetchTemplateParamValue(templateParam, params, key) .orElse(null); } + + private record AmountFormatted(BigDecimal amountRaw, String amountFormatted, String valueFormatted) {} } diff --git a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java index b65d2f311..c17ce6611 100644 --- a/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java +++ b/enrollment-server/src/test/java/com/wultra/app/enrollmentserver/impl/service/converter/MobileTokenConverterTest.java @@ -515,6 +515,150 @@ void testConvertAttributes() throws Exception { .build()), atributesIterator.next()); } + @Test + void testConvertAmount_notANumber() throws Exception { + final OperationDetailResponse operationDetail = createOperationDetailResponse(); + operationDetail.setParameters(ImmutableMap.builder() + .put("amount", "not a number") + .put("currency", "CZK") + .build()); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + operationTemplate.setAttributes(""" + [ + { + "id": "operation.amount", + "type": "AMOUNT", + "text": "Amount", + "params": { + "amount": "amount", + "currency": "currency" + } + } + ]"""); + + LocaleContextHolder.setLocale(new Locale("en")); + final Operation result = tested.convert(operationDetail, operationTemplate); + + final List attributes = result.getFormData().getAttributes(); + + assertEquals(1, attributes.size()); + final var atributesIterator = attributes.iterator(); + assertEquals(AmountAttribute.builder() + .id("operation.amount") + .label("Amount") + .amount(null) + .amountFormatted("not a number") + .currency("CZK") + .currencyFormatted("CZK") + .valueFormatted("not a number CZK") + .build(), atributesIterator.next()); + } + + @Test + void testConvertAmountConversion_sourceNotANumber() throws Exception { + final OperationDetailResponse operationDetail = createOperationDetailResponse(); + operationDetail.setParameters(ImmutableMap.builder() + .put("sourceAmount", "source not a number") + .put("sourceCurrency", "EUR") + .put("targetAmount", "1710.98") + .put("targetCurrency", "USD") + .put("dynamic", "true") + .build()); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + operationTemplate.setAttributes(""" + [ + { + "id": "operation.amountConversion", + "type": "AMOUNT_CONVERSION", + "text": "Amount Conversion", + "params": { + "dynamic": "dynamic", + "sourceAmount": "sourceAmount", + "sourceCurrency": "sourceCurrency", + "targetAmount": "targetAmount", + "targetCurrency": "targetCurrency" + } + } + ]"""); + + LocaleContextHolder.setLocale(new Locale("en")); + final Operation result = tested.convert(operationDetail, operationTemplate); + + final List attributes = result.getFormData().getAttributes(); + + assertEquals(1, attributes.size()); + final var atributesIterator = attributes.iterator(); + assertEquals(AmountConversionAttribute.builder() + .id("operation.amountConversion") + .label("Amount Conversion") + .dynamic(true) + .sourceAmount(null) + .sourceAmountFormatted("source not a number") + .sourceCurrency("EUR") + .sourceCurrencyFormatted("€") + .sourceValueFormatted("source not a number EUR") + .targetAmount(new BigDecimal("1710.98")) + .targetAmountFormatted("1,710.98") + .targetCurrency("USD") + .targetCurrencyFormatted("$") + .targetValueFormatted("$1,710.98") + .build(), atributesIterator.next()); + } + + @Test + void testConvertAmountConversion_targetNotANumber() throws Exception { + final OperationDetailResponse operationDetail = createOperationDetailResponse(); + operationDetail.setParameters(ImmutableMap.builder() + .put("sourceAmount", "1710.98") + .put("sourceCurrency", "USD") + .put("targetAmount", "target not a number") + .put("targetCurrency", "EUR") + .put("dynamic", "true") + .build()); + + final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); + operationTemplate.setAttributes(""" + [ + { + "id": "operation.amountConversion", + "type": "AMOUNT_CONVERSION", + "text": "Amount Conversion", + "params": { + "dynamic": "dynamic", + "sourceAmount": "sourceAmount", + "sourceCurrency": "sourceCurrency", + "targetAmount": "targetAmount", + "targetCurrency": "targetCurrency" + } + } + ]"""); + + LocaleContextHolder.setLocale(new Locale("en")); + final Operation result = tested.convert(operationDetail, operationTemplate); + + final List attributes = result.getFormData().getAttributes(); + + assertEquals(1, attributes.size()); + final var atributesIterator = attributes.iterator(); + assertEquals(AmountConversionAttribute.builder() + .id("operation.amountConversion") + .label("Amount Conversion") + .dynamic(true) + .sourceAmount(new BigDecimal("1710.98")) + .sourceAmountFormatted("1,710.98") + .sourceCurrency("USD") + .sourceCurrencyFormatted("$") + .sourceValueFormatted("$1,710.98") + .targetAmount(null) + .targetAmountFormatted("target not a number") + .targetCurrency("EUR") + .targetCurrencyFormatted("€") + .targetValueFormatted("target not a number EUR") + .build(), atributesIterator.next()); + } + @Test void testConvertImageAttributeWithoutOriginalUrl() throws Exception { final OperationDetailResponse operationDetail = createOperationDetailResponse(); From 452f27f9d8a52376b463bd2a41b84bbe01bd7a7d Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Mon, 8 Jan 2024 08:08:21 +0100 Subject: [PATCH 50/52] Fix #982: Update Docker dependencies --- Dockerfile | 6 +++--- docs-private/Developer-How-To-Start.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index ca781b412..4f51ea7d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ibm-semeru-runtimes:open-17.0.8_7-jre +FROM ibm-semeru-runtimes:open-17.0.9_9-jre LABEL maintainer="petr@wultra.com" # Prepare environment variables @@ -8,7 +8,7 @@ ENV JAVA_HOME=/opt/java/openjdk \ PKG_RELEASE=1~jammy \ TOMCAT_HOME=/usr/local/tomcat \ TOMCAT_MAJOR=10 \ - TOMCAT_VERSION=10.1.13 \ + TOMCAT_VERSION=10.1.17 \ TZ=UTC ENV PATH=$PATH:$LB_HOME:$TOMCAT_HOME/bin @@ -20,7 +20,7 @@ RUN apt-get -y update \ # Install tomcat RUN curl -jkSL -o /tmp/apache-tomcat.tar.gz http://archive.apache.org/dist/tomcat/tomcat-${TOMCAT_MAJOR}/v${TOMCAT_VERSION}/bin/apache-tomcat-${TOMCAT_VERSION}.tar.gz \ - && [ "406c0c367ac6ad95bb724ecc3a3c340ad7ded8c62287d657811eeec496eaaca1f5add52d2f46111da1426ae67090c543f6deccfeb5fdf4bdae32f9b39e773265 /tmp/apache-tomcat.tar.gz" = "$(sha512sum /tmp/apache-tomcat.tar.gz)" ] \ + && [ "ff9670f9cd49a604e47edfbcfb5855fe59342048c3278ea8736276b51327adf2d076973f3ad1b8aa7870ef26c28cf7111527be810b445c9927f2a457795f5cb6 /tmp/apache-tomcat.tar.gz" = "$(sha512sum /tmp/apache-tomcat.tar.gz)" ] \ && gunzip /tmp/apache-tomcat.tar.gz \ && tar -C /opt -xf /tmp/apache-tomcat.tar \ && ln -s /opt/apache-tomcat-$TOMCAT_VERSION $TOMCAT_HOME diff --git a/docs-private/Developer-How-To-Start.md b/docs-private/Developer-How-To-Start.md index 3a0eaad72..1ca53619d 100644 --- a/docs-private/Developer-How-To-Start.md +++ b/docs-private/Developer-How-To-Start.md @@ -36,20 +36,20 @@ mvn clean package ### Build the docker image ```shell -docker build . -t enrollment-server:1.5.0 +docker build . -t enrollment-server:1.6.0 ``` ### Prepare environment variables -* Copy `deploy/env.list.tmp` to `./env.list` and edit the values to use it via `docker run --env-file env.list IMAGE` -* Or set environment variables via `docker run -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' IMAGE` +* Copy `deploy/env.list.tmp` to `./env.list` and edit the values to use it via `docker run --env-file env.list enrollment-server:1.6.0` +* Or set environment variables via `docker run -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' enrollment-server:1.6.0` ### Run the docker image ```shell -docker run -p 80:8080 -e ENROLLMENT_SERVER_DATASOURCE_URL='jdbc:postgresql://host.docker.internal:5432/powerauth' -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' -e ENROLLMENT_SERVER_DATASOURCE_PASSWORD='' enrollment-server:1.5.0 +docker run -p 80:8080 -e ENROLLMENT_SERVER_DATASOURCE_URL='jdbc:postgresql://host.docker.internal:5432/powerauth' -e ENROLLMENT_SERVER_DATASOURCE_USERNAME='powerauth' -e ENROLLMENT_SERVER_DATASOURCE_PASSWORD='' enrollment-server:1.6.0 ``` From ff2d2020de981b5379f56b25b482ab4690390502 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Mon, 8 Jan 2024 08:54:03 +0100 Subject: [PATCH 51/52] Fix #984: Set release version to 1.6.0 --- enrollment-server-api-model/pom.xml | 2 +- enrollment-server-onboarding-adapter-mock/pom.xml | 2 +- enrollment-server-onboarding-api-model/pom.xml | 2 +- enrollment-server-onboarding-api/pom.xml | 2 +- enrollment-server-onboarding-common/pom.xml | 2 +- enrollment-server-onboarding-domain-model/pom.xml | 2 +- enrollment-server-onboarding-provider-innovatrics/pom.xml | 2 +- enrollment-server-onboarding-provider-iproov/pom.xml | 2 +- enrollment-server-onboarding-provider-zenid/pom.xml | 2 +- enrollment-server-onboarding/pom.xml | 2 +- enrollment-server/pom.xml | 2 +- mtoken-model/pom.xml | 2 +- pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/enrollment-server-api-model/pom.xml b/enrollment-server-api-model/pom.xml index a798a3719..d4fde669a 100644 --- a/enrollment-server-api-model/pom.xml +++ b/enrollment-server-api-model/pom.xml @@ -30,7 +30,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 diff --git a/enrollment-server-onboarding-adapter-mock/pom.xml b/enrollment-server-onboarding-adapter-mock/pom.xml index 99f5897a6..e3216d78d 100644 --- a/enrollment-server-onboarding-adapter-mock/pom.xml +++ b/enrollment-server-onboarding-adapter-mock/pom.xml @@ -24,7 +24,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 enrollment-server-onboarding-adapter-mock diff --git a/enrollment-server-onboarding-api-model/pom.xml b/enrollment-server-onboarding-api-model/pom.xml index b22e220c9..aea8d0ba1 100644 --- a/enrollment-server-onboarding-api-model/pom.xml +++ b/enrollment-server-onboarding-api-model/pom.xml @@ -7,7 +7,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 enrollment-server-onboarding-api-model diff --git a/enrollment-server-onboarding-api/pom.xml b/enrollment-server-onboarding-api/pom.xml index 48cb44c2e..82203d276 100644 --- a/enrollment-server-onboarding-api/pom.xml +++ b/enrollment-server-onboarding-api/pom.xml @@ -25,7 +25,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 com.wultra.security diff --git a/enrollment-server-onboarding-common/pom.xml b/enrollment-server-onboarding-common/pom.xml index d6d88b13e..7c54ec298 100644 --- a/enrollment-server-onboarding-common/pom.xml +++ b/enrollment-server-onboarding-common/pom.xml @@ -24,7 +24,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 enrollment-server-onboarding-common diff --git a/enrollment-server-onboarding-domain-model/pom.xml b/enrollment-server-onboarding-domain-model/pom.xml index 05d934ce1..cc0fb0b21 100644 --- a/enrollment-server-onboarding-domain-model/pom.xml +++ b/enrollment-server-onboarding-domain-model/pom.xml @@ -30,7 +30,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 diff --git a/enrollment-server-onboarding-provider-innovatrics/pom.xml b/enrollment-server-onboarding-provider-innovatrics/pom.xml index bec000cb1..1fd6417a4 100644 --- a/enrollment-server-onboarding-provider-innovatrics/pom.xml +++ b/enrollment-server-onboarding-provider-innovatrics/pom.xml @@ -25,7 +25,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 com.wultra.security diff --git a/enrollment-server-onboarding-provider-iproov/pom.xml b/enrollment-server-onboarding-provider-iproov/pom.xml index 3822d8b36..505cfa31c 100644 --- a/enrollment-server-onboarding-provider-iproov/pom.xml +++ b/enrollment-server-onboarding-provider-iproov/pom.xml @@ -25,7 +25,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 com.wultra.security diff --git a/enrollment-server-onboarding-provider-zenid/pom.xml b/enrollment-server-onboarding-provider-zenid/pom.xml index 00c1a27ca..e6b8b938a 100644 --- a/enrollment-server-onboarding-provider-zenid/pom.xml +++ b/enrollment-server-onboarding-provider-zenid/pom.xml @@ -25,7 +25,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 com.wultra.security diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index 3c9f79bb3..e3f495278 100644 --- a/enrollment-server-onboarding/pom.xml +++ b/enrollment-server-onboarding/pom.xml @@ -29,7 +29,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index bd0e7e310..bea22f846 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -30,7 +30,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 diff --git a/mtoken-model/pom.xml b/mtoken-model/pom.xml index a95dd7844..880febd13 100644 --- a/mtoken-model/pom.xml +++ b/mtoken-model/pom.xml @@ -26,7 +26,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 diff --git a/pom.xml b/pom.xml index 0cde978f1..50ea871fe 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ com.wultra.security enrollment-server-parent - 1.6.0-SNAPSHOT + 1.6.0 pom From 41b9ea14eab4c436e57dd9c7b56f6bb6fb600060 Mon Sep 17 00:00:00 2001 From: Lubos Racansky Date: Tue, 9 Jan 2024 07:11:56 +0100 Subject: [PATCH 52/52] Remove commented out code Follow-up to #907 --- .../provider/innovatrics/InnovatricsApiService.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java index 916c08fb7..bc3caebde 100644 --- a/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java +++ b/enrollment-server-onboarding-provider-innovatrics/src/main/java/com/wultra/app/onboardingserver/provider/innovatrics/InnovatricsApiService.java @@ -222,14 +222,6 @@ public CreateSelfieResponse createSelfie(final String customerId, final String l } } - // TODO remove - temporal test call -// @PostConstruct -// public void testCall() throws RestClientException { -// logger.info("Trying a test call"); -// final ResponseEntity response = restClient.get("/api/v1/metadata", STRING_TYPE_REFERENCE); -// logger.info("Result of test call: {}", response.getBody()); -// } - /** * Create a new customer resource. * @param ownerId owner identification.