diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9f62dc964..ae5e8ae7a 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,4 +18,5 @@ jobs: languages: "['java']" # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Use only 'java' to analyze code written in Java, Kotlin or both - # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both \ No newline at end of file + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + java_version: 21 \ No newline at end of file diff --git a/.github/workflows/maven-deploy.yml b/.github/workflows/maven-deploy.yml index 9a1690fd0..35a0646df 100644 --- a/.github/workflows/maven-deploy.yml +++ b/.github/workflows/maven-deploy.yml @@ -34,6 +34,7 @@ jobs: with: environment: internal-publish release_type: snapshot + java_version: 21 secrets: username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} @@ -45,6 +46,7 @@ jobs: with: environment: ${{ inputs.environment }} release_type: ${{ inputs.release_type }} + java_version: 21 secrets: username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} diff --git a/.github/workflows/maven-test.yml b/.github/workflows/maven-test.yml index 6bdada9fe..33932bf61 100644 --- a/.github/workflows/maven-test.yml +++ b/.github/workflows/maven-test.yml @@ -15,4 +15,6 @@ on: jobs: maven-tests: uses: wultra/wultra-infrastructure/.github/workflows/maven-test.yml@develop - secrets: inherit \ No newline at end of file + secrets: inherit + with: + java_version: 21 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4f51ea7d4..e2a03d33a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ibm-semeru-runtimes:open-17.0.9_9-jre +FROM ibm-semeru-runtimes:open-21.0.2_13-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.17 \ + TOMCAT_VERSION=10.1.19 \ 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 \ - && [ "ff9670f9cd49a604e47edfbcfb5855fe59342048c3278ea8736276b51327adf2d076973f3ad1b8aa7870ef26c28cf7111527be810b445c9927f2a457795f5cb6 /tmp/apache-tomcat.tar.gz" = "$(sha512sum /tmp/apache-tomcat.tar.gz)" ] \ + && [ "7264da6196a510b0bba74469d215d61a464331302239256477f78b6bec067f7f4d90f671b96a440061ae0e20d16b1be8ca1dbd547dab9927383366dbc677f590 /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/deploy/enrollment-server.xml b/deploy/enrollment-server.xml index c9a24edc2..8a20f2b6d 100644 --- a/deploy/enrollment-server.xml +++ b/deploy/enrollment-server.xml @@ -29,6 +29,12 @@ + + + + + + diff --git a/deploy/env.list.tmp b/deploy/env.list.tmp index 32128f21a..8c4ed3da2 100644 --- a/deploy/env.list.tmp +++ b/deploy/env.list.tmp @@ -5,6 +5,12 @@ ENROLLMENT_SERVER_PUSH_SERVER_URL= ENROLLMENT_SERVER_MTOKEN_ENABLED=true ENROLLMENT_SERVER_INBOX_ENABLED=true ENROLLMENT_SERVER_ACTIVATION_SPAWN_ENABLED=false +ENROLLMENT_SERVER_ADMIN_ENABLED=false +ENROLLMENT_SERVER_AUTH_TYPE=NONE +ENROLLMENT_SERVER_SECURITY_AUTH_HTTP_BASIC_USER_NAME= +ENROLLMENT_SERVER_SECURITY_AUTH_HTTP_BASIC_USER_PASSWORD= +ENROLLMENT_SERVER_SECURITY_AUTH_OIDC_ISSUER_URI= +ENROLLMENT_SERVER_SECURITY_AUTH_OIDC_AUDIENCES= ENROLLMENT_SERVER_CORRELATION_HEADER_ENABLED=false ENROLLMENT_SERVER_CORRELATION_HEADER_NAME=X-Correlation-ID ENROLLMENT_SERVER_CORRELATION_HEADER_VALUE_VALIDATION_REGEXP=[a-zA-Z0-9\\-]{8,1024} diff --git a/docs/Configuration-Properties.md b/docs/Configuration-Properties.md index 46718a263..47b9b4559 100644 --- a/docs/Configuration-Properties.md +++ b/docs/Configuration-Properties.md @@ -9,7 +9,6 @@ The Enrollment Server uses the following public configuration properties: | `spring.datasource.url` | `_empty_` | Database JDBC URL | | `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.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 | @@ -30,11 +29,17 @@ The Enrollment Server uses the following public configuration properties: ## Enrollment Server Configuration -| Property | Default | Note | -|---|---|---| -| `enrollment-server.mtoken.enabled` | `true` | Publishing of Mobile Token endpoints can be enabled or disabled using this property. | -| `enrollment-server.inbox.enabled` | `true` | Publishing of Inbox endpoints can be enabled or disabled using this property. | -| `enrollment-server.activation-spawn.enabled` | `false` | The activation spawn functionality can be enabled or disabled using this property. | +| Property | Default | Note | +|---------------------------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `enrollment-server.mtoken.enabled` | `true` | Publishing of Mobile Token endpoints can be enabled or disabled using this property. | +| `enrollment-server.inbox.enabled` | `true` | Publishing of Inbox endpoints can be enabled or disabled using this property. | +| `enrollment-server.activation-spawn.enabled` | `false` | The activation spawn functionality can be enabled or disabled using this property. | +| `enrollment-server.admin.enabled` | `false` | The admin API can be enabled or disabled using this property. | +| `enrollment-server.auth-type` | `NONE` | `BASIC_HTTP` for basic HTTP authentication or `OIDC` for OpenID Connect. If authentication enabled, the corresponding properties bellow must be configured. | +| `spring.security.user.name` | | Basic HTTP property, user name | +| `spring.security.user.password` | | Basic HTTP property, user password `{id}encodedPassword`, see [Spring Password Storage Format](https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html#authentication-password-storage-dpe-format). | +| `spring.security.oauth2.resource-server.jwt.issuer-uri` | | OIDC property, URL of the provider, e.g. `https://sts.windows.net/example/` | +| `spring.security.oauth2.resource-server.jwt.audiences` | | OIDC property, a comma-separated list of allowed `aud` JWT claim values to be validated. | ## UserInfoProvider Configuration @@ -63,6 +68,8 @@ logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS ## Monitoring and Observability - +| Property | Default | Note | +|-------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `management.tracing.sampling.probability` | `1.0` | Specifies the proportion of requests that are sampled for tracing. A value of 1.0 means that 100% of requests are sampled, while a value of 0 effectively disables tracing. | 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/Migration-Instructions.md b/docs/Migration-Instructions.md index 0bf91d4af..740c7d30d 100644 --- a/docs/Migration-Instructions.md +++ b/docs/Migration-Instructions.md @@ -2,6 +2,7 @@ This page contains PowerAuth Enrollment Server migration instructions. +- [PowerAuth Enrollment Server 1.7.0](./PowerAuth-Enrollment-Server-1.7.0.md) - [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.7.0.md b/docs/PowerAuth-Enrollment-Server-1.7.0.md new file mode 100644 index 000000000..f6b607ef8 --- /dev/null +++ b/docs/PowerAuth-Enrollment-Server-1.7.0.md @@ -0,0 +1,12 @@ +# Migration from 1.6.x to 1.7.x + +This guide contains instructions for migration from PowerAuth Enrollment Server version `1.6.x` to version `1.7.0`. + + +## REST API + + +### Register for Push Messages (Token) + +The endpoint `POST /api/push/device/register/token` now strictly validates `platform` against values `ios`, `android` or `huawei`. +If you use the PowerAuth SDK, you should not be affected. diff --git a/docs/onboarding/Configuration-Properties.md b/docs/onboarding/Configuration-Properties.md index 799d45a15..3f1881e69 100644 --- a/docs/onboarding/Configuration-Properties.md +++ b/docs/onboarding/Configuration-Properties.md @@ -9,7 +9,6 @@ The Onboarding Server uses the following public configuration properties: | `spring.datasource.url` | `jdbc:postgresql://localhost:5432/powerauth` | Database JDBC URL | | `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.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 | @@ -46,7 +45,6 @@ The Onboarding Server uses the following public configuration properties: | `enrollment-server-onboarding.identity-verification.otp.enabled` | `true` | Whether OTP verification is enabled during identity verification. | | `enrollment-server-onboarding.identity-verification.max-failed-attempts` | `5` | Maximum failed attempts for identity verification. | | `enrollment-server-onboarding.identity-verification.max-failed-attempts-document-upload` | `5` | Maximum failed attempts for document upload. | -| `enrollment-server-onboarding.client-evaluation.max-failed-attempts` | `5` | Maximum failed attempts for client evaluation. | ## Digital Onboarding Adapter Configuration @@ -69,6 +67,7 @@ The Onboarding Server uses the following public configuration properties: | Property | Default | Note | |---|---|---| | `enrollment-server-onboarding.client-evaluation.max-failed-attempts` | 5 | Number of maximum failed attempts for client evaluation. | +| `enrollment-server-onboarding.client-evaluation.include-extracted-data` | `false` | Include extracted data to the evaluate client request. The format of extracted data is defined by the provider of document verification. | ## Document Verification Provider Configuration @@ -170,6 +169,8 @@ logging.pattern.console=%clr(%d{${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS ## Monitoring and Observability - +| Property | Default | Note | +|-------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `management.tracing.sampling.probability` | `1.0` | Specifies the proportion of requests that are sampled for tracing. A value of 1.0 means that 100% of requests are sampled, while a value of 0 effectively disables tracing. | 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-Verification-Providers.md b/docs/onboarding/Configuration-Verification-Providers.md index ed358b8bd..09d045190 100644 --- a/docs/onboarding/Configuration-Verification-Providers.md +++ b/docs/onboarding/Configuration-Verification-Providers.md @@ -11,7 +11,7 @@ The document verification process is currently supported for following providers ### ZenID -#### Configuration - API key +#### API key The authorization of all API calls is secured by an API key value. It has to be sent as the `Authorization: api_key VALUE` header value. Check the bottom of the `Manual/Configuration` page for more details. @@ -21,7 +21,7 @@ The API key value can be configured/get from the `Access` page configuration: - Condition: `ApiKeyEqualsValue` - Value: the value here is the value of the API key -#### Configuration - Validators +#### Validators It is recommended to create a custom validation profile. The sensitivity of selected validators can be tuned-up or disabled completely at the `Sensitivity` page. The profile can be then set as the default or specified in the configuration properties. @@ -32,6 +32,41 @@ When calling `document-verification/init-sdk` following implementation fields ar - Init token - send a token value `sdk-init-token` in the request body `attributes` map field - SDK response - receive the value under `zenid-sdk-init-response` from the response `attributes` map field +### Innovatrics + +Innovatrics documentation for developers can be found at [this link](https://developers.innovatrics.com/digital-onboarding/technical/remote/dot-dis/latest/documentation/). + +#### OCR Threshold + +During a document validation Innovatrics provides a list of fields extracted from the document, that have OCR +confidence lower than configurable threshold. If the list is not empty, there is a high probability that some +information is read incorrectly. For that reason, this document will be rejected. The OCR confidence threshold is `0.92` +by default, and can be tuned using `innovatrics.dot.dis.customer.document.inspection.ocr-text-field-threshold`. + +#### Text Consistency + +For each document Innovatrics tries to read visual zone, machine-readable zone and barcode. These isolated parts are +cross-checked during a document validation by Innovatrics. If there are inconsistency between visual zone and +machine-readable zone, or between visual-zone and barcode, the document will be rejected. However, some editions of +identification documents are inconsistent by design. To prevent false rejection of those document modify the +configuration. +Following example excludes `issuingAuthority` field of Czech identity card 2005 edition from text consistency check: + +```yml +innovatrics: + dot: + dis: + customer: + document: + inspection: + text-consistency-check: + CZE_identity-card_2005-01-01: + exclusions: + - issuingAuthority +``` + +The format of the document name is `{country}_{type}_{edition}` according to the response of `/metadata` request. + ## Presence Check The document verification process is currently supported for following providers: @@ -39,7 +74,7 @@ The document verification process is currently supported for following providers - [Innovatrics](https://www.innovatrics.com/) - use value `innovatrics` in configuration - Mock - useful for simple testing and local runs - use value `mock` in configuration -#### Configuration +### iProov There are a few needed configuration changes to bring a successful integration. All the following configuration tuning has to be requested from the iProov's [support team](https://iproov.freshdesk.com/support/login) on a per-service basis: diff --git a/enrollment-server-api-model/pom.xml b/enrollment-server-api-model/pom.xml index d4fde669a..18c368b40 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 + 1.7.0 @@ -43,6 +43,11 @@ io.swagger.core.v3 swagger-annotations-jakarta + + + com.fasterxml.jackson.core + jackson-annotations + diff --git a/enrollment-server-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/enrollment/request/PushRegisterRequest.java b/enrollment-server-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/enrollment/request/PushRegisterRequest.java index 21dee883a..11121cbe3 100644 --- a/enrollment-server-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/enrollment/request/PushRegisterRequest.java +++ b/enrollment-server-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/enrollment/request/PushRegisterRequest.java @@ -18,19 +18,44 @@ package com.wultra.app.enrollmentserver.api.model.enrollment.request; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Data; +import lombok.ToString; /** - * Class representing a device registration request. The supported platform - * values are 'ios' and 'android'. The push token is the value received from - * APNS or FCM services without any modification. + * Class representing a device registration request. * * @author Petr Dvorak, petr@wultra.com */ @Data public class PushRegisterRequest { - private String platform; + /** + * The platform. + */ + @NotNull + private Platform platform; + + /** + * The push token is the value received from APNS, FCM, or HMS services without any modification. + */ + @NotBlank + @ToString.Exclude + @Schema(description = "The push token is the value received from APNS, FCM, or HMS services without any modification.") private String token; + public enum Platform { + @JsonProperty("ios") + IOS, + + @JsonProperty("android") + ANDROID, + + @JsonProperty("huawei") + HUAWEI + } + } diff --git a/enrollment-server-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/enrollment/response/TemplateListResponse.java b/enrollment-server-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/enrollment/response/TemplateListResponse.java new file mode 100644 index 000000000..248f0e423 --- /dev/null +++ b/enrollment-server-api-model/src/main/java/com/wultra/app/enrollmentserver/api/model/enrollment/response/TemplateListResponse.java @@ -0,0 +1,41 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2024 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.api.model.enrollment.response; + +import lombok.Builder; +import lombok.EqualsAndHashCode; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; + +/** + * Template list response. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@EqualsAndHashCode(callSuper = true) +public class TemplateListResponse extends ArrayList { + + @Serial + private static final long serialVersionUID = -5446919236567435144L; + + @Builder + public record TemplateDetail(String name, String title, String message, List attributes, String language) { + } +} diff --git a/enrollment-server-onboarding-adapter-mock/pom.xml b/enrollment-server-onboarding-adapter-mock/pom.xml index e3216d78d..1ed13fcba 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 + 1.7.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 aea8d0ba1..3f532b6bb 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 + 1.7.0 enrollment-server-onboarding-api-model diff --git a/enrollment-server-onboarding-api/pom.xml b/enrollment-server-onboarding-api/pom.xml index 82203d276..05397e10b 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 + 1.7.0 com.wultra.security diff --git a/enrollment-server-onboarding-common/pom.xml b/enrollment-server-onboarding-common/pom.xml index 7c54ec298..99f9e0cf0 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 + 1.7.0 enrollment-server-onboarding-common diff --git a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/IdentityVerificationRepository.java b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/IdentityVerificationRepository.java index 51b51a942..a1e19ce6e 100644 --- a/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/IdentityVerificationRepository.java +++ b/enrollment-server-onboarding-common/src/main/java/com/wultra/app/onboardingserver/common/database/IdentityVerificationRepository.java @@ -55,28 +55,36 @@ public interface IdentityVerificationRepository extends CrudRepository streamAllIdentityVerificationsToChangeState(); - + Stream streamAllIdentityVerificationsToChangeState(final String documentVerificationProvider); /** * Return identity verification IDs by the given process ID. Include only not yet finished entities. diff --git a/enrollment-server-onboarding-common/src/test/resources/application-test.properties b/enrollment-server-onboarding-common/src/test/resources/application-test.properties index 308c2b3ac..98bb47ca1 100644 --- a/enrollment-server-onboarding-common/src/test/resources/application-test.properties +++ b/enrollment-server-onboarding-common/src/test/resources/application-test.properties @@ -1,5 +1,4 @@ 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-domain-model/pom.xml b/enrollment-server-onboarding-domain-model/pom.xml index cc0fb0b21..90dac5e39 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 + 1.7.0 @@ -38,6 +38,19 @@ io.getlime.security powerauth-java-crypto + + + + org.bouncycastle + bcprov-jdk18on + + + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/OwnerId.java b/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/OwnerId.java index dc5df8145..3992bfef5 100644 --- a/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/OwnerId.java +++ b/enrollment-server-onboarding-domain-model/src/main/java/com/wultra/app/enrollmentserver/model/integration/OwnerId.java @@ -17,13 +17,14 @@ */ package com.wultra.app.enrollmentserver.model.integration; -import com.google.common.io.BaseEncoding; import io.getlime.security.powerauth.crypto.lib.util.Hash; import lombok.AccessLevel; import lombok.Data; import lombok.Setter; import lombok.ToString; +import org.bouncycastle.util.encoders.Base32; +import java.nio.charset.StandardCharsets; import java.util.Date; /** @@ -73,9 +74,8 @@ public String getUserIdSecured() { throw new IllegalStateException("Missing userId value"); } if (userIdSecured == null) { - userIdSecured = BaseEncoding.base32() - .omitPadding() - .encode(Hash.sha256(userId)); + userIdSecured = new String(Base32.encode(Hash.sha256(userId)), StandardCharsets.UTF_8) + .replace("=", ""); if (userIdSecured.length() > USER_ID_MAX_LENGTH) { userIdSecured = userIdSecured.substring(0, USER_ID_MAX_LENGTH); } diff --git a/enrollment-server-onboarding-domain-model/src/test/java/com/wultra/app/enrollmentserver/model/integration/OwnerIdTest.java b/enrollment-server-onboarding-domain-model/src/test/java/com/wultra/app/enrollmentserver/model/integration/OwnerIdTest.java new file mode 100644 index 000000000..b763f6511 --- /dev/null +++ b/enrollment-server-onboarding-domain-model/src/test/java/com/wultra/app/enrollmentserver/model/integration/OwnerIdTest.java @@ -0,0 +1,40 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2024 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.model.integration; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test for {@link OwnerId}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +class OwnerIdTest { + + @Test + void testUserIdSecured() { + final OwnerId tested = new OwnerId(); + tested.setUserId("Joe"); + + final String result = tested.getUserIdSecured(); + + assertEquals("NXMLPV6TYXCGRGZT4UNZ6EF4NKN6RH7I7IVBE7EMNQB42BOWRLHA", result); + } +} diff --git a/enrollment-server-onboarding-provider-innovatrics/pom.xml b/enrollment-server-onboarding-provider-innovatrics/pom.xml index 1fd6417a4..ac6c40290 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 + 1.7.0 com.wultra.security 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 5f660561e..d2f6a81db 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 @@ -20,11 +20,10 @@ 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.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; @@ -33,10 +32,10 @@ import com.wultra.app.onboardingserver.provider.innovatrics.model.api.*; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; 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.*; import java.util.stream.Collectors; @@ -99,7 +98,7 @@ public DocumentsSubmitResult submitDocuments(OwnerId id, List } final Optional primaryPage = results.getResults().stream() - .filter(result -> Strings.isNullOrEmpty(result.getRejectReason()) && Strings.isNullOrEmpty(result.getErrorDetail())) + .filter(result -> StringUtils.isBlank(result.getRejectReason()) && StringUtils.isBlank(result.getErrorDetail())) .findFirst(); if (primaryPage.isPresent()) { @@ -132,9 +131,9 @@ public DocumentsVerificationResult verifyDocuments(OwnerId id, List uplo final String rejectReasons = results.getResults().stream() .map(DocumentVerificationResult::getRejectReason) - .filter(StringUtils::hasText) + .filter(StringUtils::isNotBlank) .collect(Collectors.joining(";")); - if (StringUtils.hasText(rejectReasons)) { + if (StringUtils.isNotBlank(rejectReasons)) { logger.debug("Some documents were rejected: rejectReasons={}, {}", rejectReasons, id); results.setStatus(DocumentVerificationStatus.REJECTED); results.setRejectReason(rejectReasons); @@ -172,7 +171,7 @@ public void cleanupDocuments(OwnerId id, List uploadIds) throws RemoteCo public List parseRejectionReasons(DocumentResultEntity docResult) throws DocumentVerificationException { logger.debug("Parsing rejection reasons of {}", docResult); final String rejectionReasons = docResult.getRejectReason(); - if (!StringUtils.hasText(rejectionReasons)) { + if (StringUtils.isBlank(rejectionReasons)) { return Collections.emptyList(); } 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 8b97aa52e..cdf5061cc 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 @@ -19,7 +19,6 @@ 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; @@ -125,7 +124,7 @@ private static String fetchCustomerId(final OwnerId id, final IdentityVerificati } final String customerId = (String) sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE); - if (Strings.isNullOrEmpty(customerId)) { + if (StringUtils.isBlank(customerId)) { throw new IdentityVerificationException("Missing a customer ID value for calling Innovatrics, " + id); } return customerId; 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 007c06c6a..b4dca0c5a 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,7 +17,6 @@ */ 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; @@ -31,6 +30,7 @@ import com.wultra.app.onboardingserver.provider.innovatrics.model.api.SelfieSimilarityWith; 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.Component; @@ -151,7 +151,7 @@ private static Optional fail(final String errorDetail) { private static String fetchCustomerId(final OwnerId id, final SessionInfo sessionInfo) throws PresenceCheckException { final String customerId = (String) sessionInfo.getSessionAttributes().get(SessionInfo.ATTRIBUTE_PRIMARY_DOCUMENT_REFERENCE); - if (Strings.isNullOrEmpty(customerId)) { + if (StringUtils.isBlank(customerId)) { throw new PresenceCheckException("Missing a customer ID value for calling Innovatrics, " + id); } return customerId; diff --git a/enrollment-server-onboarding-provider-iproov/pom.xml b/enrollment-server-onboarding-provider-iproov/pom.xml index 505cfa31c..4cf145984 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 + 1.7.0 com.wultra.security 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 d34579224..2521216b9 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 @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -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; @@ -34,6 +33,7 @@ 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.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -161,7 +161,7 @@ public SessionInfo startPresenceCheck(OwnerId id) throws PresenceCheckException, @Override public PresenceCheckResult getResult(OwnerId id, SessionInfo sessionInfo) throws PresenceCheckException, RemoteCommunicationException { final String token = (String) sessionInfo.getSessionAttributes().get(VERIFICATION_TOKEN); - if (Strings.isNullOrEmpty(token)) { + if (StringUtils.isBlank(token)) { throw new PresenceCheckException("Missing a token value for verification validation in iProov, " + id); } diff --git a/enrollment-server-onboarding-provider-zenid/pom.xml b/enrollment-server-onboarding-provider-zenid/pom.xml index e6b8b938a..4cfed4951 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 + 1.7.0 com.wultra.security 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 012d48715..1d5838598 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 @@ -20,22 +20,22 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -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.enumeration.DocumentVerificationStatus; 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.DocumentVerificationRepository; 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.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; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -336,7 +336,7 @@ public List parseRejectionReasons(DocumentResultEntity docResult) throws @Override public VerificationSdkInfo initVerificationSdk(OwnerId id, Map initAttributes) throws RemoteCommunicationException, DocumentVerificationException { - Preconditions.checkArgument(initAttributes.containsKey(SDK_INIT_TOKEN), "Missing initialization token for ZenID SDK"); + Validate.isTrue(initAttributes.containsKey(SDK_INIT_TOKEN), "Missing initialization token for ZenID SDK"); String token = initAttributes.get(SDK_INIT_TOKEN); ResponseEntity responseEntity; diff --git a/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiService.java b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiService.java index c98964a2e..200cf86f8 100644 --- a/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiService.java +++ b/enrollment-server-onboarding-provider-zenid/src/main/java/com/wultra/app/onboardingserver/provider/zenid/ZenidRestApiService.java @@ -17,7 +17,6 @@ */ 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; @@ -27,6 +26,7 @@ import com.wultra.core.rest.client.base.RestClientException; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.Validate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -104,7 +104,7 @@ public ZenidRestApiService( */ public ResponseEntity uploadSample(OwnerId ownerId, SubmittedDocument document) throws RestClientException { - Preconditions.checkNotNull(document.getPhoto(), "Missing photo in " + document); + Validate.notNull(document.getPhoto(), "Missing photo in " + document); final MultiValueMap queryParams = buildQueryParams(ownerId, document); @@ -148,9 +148,8 @@ public ResponseEntity syncSample(String documentId * @param sampleIds Ids of previously uploaded samples. * @return Response entity with the investigation result */ - public ResponseEntity investigateSamples(List sampleIds) - throws RestClientException { - Preconditions.checkArgument(sampleIds.size() > 0, "Missing sample ids for investigation"); + public ResponseEntity investigateSamples(List sampleIds) throws RestClientException { + Validate.notEmpty(sampleIds, "Missing sample ids for investigation"); MultiValueMap queryParams = new LinkedMultiValueMap<>(); sampleIds.forEach(sampleId -> queryParams.add("sampleIDs", sampleId)); diff --git a/enrollment-server-onboarding/pom.xml b/enrollment-server-onboarding/pom.xml index e3f495278..4ca7e8b20 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 + 1.7.0 @@ -101,6 +101,11 @@ spring-boot-starter-validation + + com.github.ben-manes.caffeine + caffeine + + jakarta.servlet jakarta.servlet-api @@ -148,6 +153,16 @@ micrometer-registry-prometheus + + io.projectreactor + reactor-core-micrometer + + + + io.micrometer + micrometer-tracing-bridge-otel + + org.springframework.boot @@ -185,10 +200,6 @@ - - org.apache.maven.plugins - maven-war-plugin - org.springframework.boot spring-boot-maven-plugin diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/IdentityVerificationConfig.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/IdentityVerificationConfig.java index 37e1d40be..379db3cd0 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/IdentityVerificationConfig.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/configuration/IdentityVerificationConfig.java @@ -79,6 +79,9 @@ public class IdentityVerificationConfig { @Value("${enrollment-server-onboarding.client-evaluation.max-failed-attempts:5}") private int clientEvaluationMaxFailedAttempts; + @Value("${enrollment-server-onboarding.client-evaluation.include-extracted-data:false}") + private boolean sendingExtractedDataEnabled; + @PostConstruct void validate() { // Once in the future, we may replace OTP in SCA by NFC document reading 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 692830985..b4b384d69 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 @@ -17,18 +17,18 @@ */ package com.wultra.app.onboardingserver.docverify.mock.provider; -import com.google.common.base.Ascii; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; 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.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.docverify.mock.MockConst; -import com.wultra.app.onboardingserver.api.errorhandling.DocumentVerificationException; -import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; @@ -65,11 +65,12 @@ public class WultraMockDocumentVerificationProvider implements DocumentVerificat public WultraMockDocumentVerificationProvider() { logger.warn("Using mocked version of {}", DocumentVerificationProvider.class.getName()); - submittedDocs = CacheBuilder.newBuilder() + // TODO (racansky, 2024-01-04) consider removing Caffeine dependency and replace it by simple LinkedHashMap#removeEldestEntry + submittedDocs = Caffeine.newBuilder() .expireAfterWrite(Duration.ofHours(1)) .build(); - verificationUploadIds = CacheBuilder.newBuilder() + verificationUploadIds = Caffeine.newBuilder() .expireAfterWrite(Duration.ofHours(1)) .build(); } @@ -242,7 +243,7 @@ private DocumentSubmitResult toDocumentSubmitResult(final SubmittedDocument docu if (docId.startsWith("upload")) { uploadedDocId = docId; } else { - uploadedDocId = Ascii.truncate("uploaded-" + docId, 36, "..."); + uploadedDocId = StringUtils.truncate("uploaded-" + docId, 33) + "..."; } submitResult.setUploadId(uploadedDocId); submitResult.setValidationResult("{\"validationResult\": { \"data\": \"" + docId + "\" } }"); 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 94b07ebfd..ae2667f93 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 @@ -32,6 +32,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.resource.NoResourceFoundException; /** * Exception handler for RESTful API issues. @@ -236,4 +237,18 @@ public class DefaultExceptionHandler { logger.warn("Error occurred.", e); return new ErrorResponse("INVALID_REQUEST", "Invalid request sent."); } + + /** + * Exception handler for no resource found. + * + * @param e Exception. + * @return Response with error details. + */ + @ExceptionHandler(NoResourceFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public @ResponseBody ErrorResponse handleNoResourceFoundException(final NoResourceFoundException e) { + logger.warn("Error occurred when calling an API: {}", e.getMessage()); + logger.debug("Exception detail: ", e); + return new ErrorResponse("ERROR_NOT_FOUND", "Resource not found."); + } } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationService.java index 640e1cde8..cd4b19f2c 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationService.java @@ -21,7 +21,9 @@ import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin; import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase; import com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus; +import com.wultra.app.enrollmentserver.model.integration.DocumentSubmitResult; import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.service.AuditService; @@ -33,6 +35,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.List; import java.util.Set; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.ACCEPTED; @@ -86,20 +89,27 @@ public ClientEvaluationService( public void processClientEvaluation(final IdentityVerificationEntity identityVerification, final OwnerId ownerId) { logger.debug("Client evaluation started for {}", identityVerification); + final Set acceptedDocuments = selectAcceptedDocuments(identityVerification); + final String verificationId; try { - verificationId = getVerificationId(identityVerification); + verificationId = fetchVerificationId(identityVerification, acceptedDocuments); } catch (Exception e) { processVerificationIdError(identityVerification, ownerId, e); return; } - final EvaluateClientRequest request = EvaluateClientRequest.builder() + final EvaluateClientRequest.EvaluateClientRequestBuilder requestBuilder = EvaluateClientRequest.builder() .processId(identityVerification.getProcessId()) .userId(identityVerification.getUserId()) .identityVerificationId(identityVerification.getId()) .verificationId(verificationId) - .build(); + .provider(config.getDocumentVerificationProvider()); + + if (config.isSendingExtractedDataEnabled()) { + requestBuilder.extractedData(fetchDocumentsExtractedData(acceptedDocuments, identityVerification)); + } + final EvaluateClientRequest request = requestBuilder.build(); final int maxFailedAttempts = config.getClientEvaluationMaxFailedAttempts(); for (int i = 0; i < maxFailedAttempts; i++) { @@ -117,10 +127,29 @@ public void processClientEvaluation(final IdentityVerificationEntity identityVer processTooManyEvaluationError(identityVerification, ownerId); } - private static String getVerificationId(final IdentityVerificationEntity identityVerification) { - final Set verificationIds = identityVerification.getDocumentVerifications().stream() + private static Set selectAcceptedDocuments(final IdentityVerificationEntity identityVerification) { + return identityVerification.getDocumentVerifications().stream() .filter(DocumentVerificationEntity::isUsedForVerification) .filter(it -> it.getStatus() == DocumentStatus.ACCEPTED) + .collect(toSet()); + } + + private static List fetchDocumentsExtractedData(final Set documents, final IdentityVerificationEntity identityVerification) { + return documents.stream() + .map(doc -> selectLatestDocumentResult(doc, identityVerification)) + .map(DocumentResultEntity::getExtractedData) + .filter(data -> !DocumentSubmitResult.NO_DATA_EXTRACTED.equals(data)) + .toList(); + } + + private static DocumentResultEntity selectLatestDocumentResult(final DocumentVerificationEntity documentVerificationEntity, final IdentityVerificationEntity identityVerification) { + return documentVerificationEntity.getResults().stream() + .findFirst() + .orElseThrow(() -> new IllegalStateException("Missing document result for %s of %s".formatted(documentVerificationEntity, identityVerification))); + } + + private static String fetchVerificationId(final IdentityVerificationEntity identityVerification, final Set documents) { + final Set verificationIds = documents.stream() .map(DocumentVerificationEntity::getVerificationId) .collect(toSet()); 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 f4c8c2d8e..838ef2ad2 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 @@ -530,21 +530,9 @@ public Image getPhotoById(final String photoId, final OwnerId ownerId) throws Do } public List createDocsMetadata(List entities) { - List docsMetadata = new ArrayList<>(); - entities.forEach(entity -> { - DocumentMetadataResponseDto docMetadata = toDocumentMetadata(entity); - - if (DocumentStatus.REJECTED.equals(entity.getStatus())) { - List errors = collectRejectionErrors(entity); - if (docMetadata.getErrors() == null) { - docMetadata.setErrors(new ArrayList<>()); - } - docMetadata.getErrors().addAll(errors); - } - - docsMetadata.add(docMetadata); - }); - return docsMetadata; + return entities.stream() + .map(this::toDocumentMetadata) + .toList(); } /** @@ -571,7 +559,7 @@ public VerificationSdkInfo initVerificationSdk(OwnerId ownerId, Map streamAllIdentityVerificationsToChangeState() { - return identityVerificationRepository.streamAllIdentityVerificationsToChangeState(); + return identityVerificationRepository.streamAllIdentityVerificationsToChangeState(identityVerificationConfig.getDocumentVerificationProvider()); } private void moveToDocumentUpload(final OwnerId ownerId, final IdentityVerificationEntity idVerification, final IdentityVerificationStatus status) { @@ -616,8 +604,9 @@ private List collectRejectionErrors(DocumentVerificationEntity entity) { private DocumentMetadataResponseDto toDocumentMetadata(DocumentVerificationEntity entity) { DocumentMetadataResponseDto docMetadata = new DocumentMetadataResponseDto(); docMetadata.setId(entity.getId()); - if (StringUtils.isNotBlank(entity.getErrorDetail())) { - docMetadata.setErrors(List.of(entity.getErrorDetail())); + // Hide specific error reason if any. + if (StringUtils.isNotBlank(entity.getErrorDetail()) || StringUtils.isNotBlank(entity.getRejectReason())) { + docMetadata.setErrors(List.of("Error verifying the document.")); } docMetadata.setFilename(entity.getFilename()); docMetadata.setSide(entity.getSide()); 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 2b4db63ed..3dd5d7d67 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 @@ -17,7 +17,6 @@ */ package com.wultra.app.onboardingserver.impl.service; -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.api.errorhandling.PresenceCheckException; @@ -74,7 +73,7 @@ public Image upscaleImage(final OwnerId ownerId, final Image sourceImage, final final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(bufferedOutputImage, TYPE_PNG, outputStream); - final String filenamePng = Files.getNameWithoutExtension(filename) + SUFFIX_PNG; + final String filenamePng = getFilenameWithoutExtension(filename) + SUFFIX_PNG; final byte[] targetData = outputStream.toByteArray(); logger.debug("Image: {}, size: {} KB, {}", filenamePng, targetData.length / KILOBYTE, ownerId); @@ -90,4 +89,12 @@ public Image upscaleImage(final OwnerId ownerId, final Image sourceImage, final throw new PresenceCheckException("Unable to read image", e); } } + + private static String getFilenameWithoutExtension(final String filename) { + if (filename.contains(".")) { + return filename.substring(0, filename.lastIndexOf(".")); + } else { + return filename; + } + } } 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 12471f923..4c356a581 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 @@ -17,8 +17,6 @@ */ package com.wultra.app.onboardingserver.impl.service; -import com.google.common.base.Ascii; -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; @@ -40,6 +38,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; @@ -151,7 +150,7 @@ private void submitSelfiePhoto(final OwnerId ownerId, final IdentityVerification final SubmittedDocument submittedDoc = new SubmittedDocument(); // TODO use different random id approach submittedDoc.setDocumentId( - Ascii.truncate("selfie-photo-" + ownerId.getActivationId(), 36, "...") + StringUtils.truncate("selfie-photo-" + ownerId.getActivationId(), 33) + "..." ); submittedDoc.setPhoto(photo); submittedDoc.setType(DocumentType.SELFIE_PHOTO); @@ -290,7 +289,7 @@ private List getDocsWithPhoto(final IdentityVerifica } docsWithPhoto.forEach(docWithPhoto -> - Preconditions.checkNotNull(docWithPhoto.getPhotoId(), "Expected photoId value in " + docWithPhoto) + Validate.notNull(docWithPhoto.getPhotoId(), "Expected photoId value in " + docWithPhoto) ); return docsWithPhoto; 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 7dae9452b..99a91b9dd 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,7 +17,6 @@ */ 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; @@ -239,7 +238,7 @@ private void checkDocumentResubmit(final OwnerId ownerId, final DocumentSubmitRe * @param docVerification Resubmitted document. */ private void handleResubmit(final OwnerId ownerId, final String originalDocumentId, final DocumentVerificationEntity docVerification) { - if (Strings.isNullOrEmpty(originalDocumentId)) { + if (StringUtils.isBlank(originalDocumentId)) { logger.debug("Document {} is not a resubmit {}", docVerification, ownerId); return; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/util/ConditionalOnPropertyNotEmpty.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/util/ConditionalOnPropertyNotEmpty.java index 69ee892fe..bbd818c6e 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/util/ConditionalOnPropertyNotEmpty.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/impl/util/ConditionalOnPropertyNotEmpty.java @@ -18,7 +18,7 @@ package com.wultra.app.onboardingserver.impl.util; -import com.google.common.base.Strings; +import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; @@ -52,7 +52,7 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) Map attrs = metadata.getAnnotationAttributes(ConditionalOnPropertyNotEmpty.class.getName()); String propertyName = (String) Objects.requireNonNull(attrs).get("value"); String val = context.getEnvironment().getProperty(propertyName); - return !Strings.nullToEmpty(val).trim().isEmpty(); + return StringUtils.isNotBlank(val); } } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/request/EvaluateClientRequest.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/request/EvaluateClientRequest.java index b175d88de..3ee5c7599 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/request/EvaluateClientRequest.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/model/request/EvaluateClientRequest.java @@ -22,6 +22,8 @@ import com.wultra.core.annotations.PublicApi; import lombok.*; +import java.util.List; + /** * Request object for {@link OnboardingProvider#evaluateClient(EvaluateClientRequest)}. * @@ -45,4 +47,11 @@ public final class EvaluateClientRequest { @NonNull private String verificationId; + + private String provider; + + /** + * Data extracted from each document/page. Format is defined by the document verification provider used. + */ + private List extractedData; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/ClientEvaluateRequestDto.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/ClientEvaluateRequestDto.java index f636460dc..8e73a915f 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/ClientEvaluateRequestDto.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/ClientEvaluateRequestDto.java @@ -19,6 +19,8 @@ import lombok.Data; +import java.util.List; + /** * Request object for client evaluation. * @@ -39,4 +41,9 @@ class ClientEvaluateRequestDto { private String verificationId; private String provider; + + /** + * Data extracted from each document/page. Format is defined by the document verification provider used. + */ + private List extractedData; } diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProvider.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProvider.java index 814ebd876..d6607bed8 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProvider.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/provider/rest/RestOnboardingProvider.java @@ -245,6 +245,8 @@ private static ClientEvaluateRequestDto convert(final EvaluateClientRequest sour target.setIdentityVerificationId(source.getIdentityVerificationId()); target.setUserId(source.getUserId()); target.setVerificationId(source.getVerificationId()); + target.setProvider(source.getProvider()); + target.setExtractedData(source.getExtractedData()); return target; } } 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 5b1357537..56a316f50 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,7 +19,6 @@ 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; @@ -69,8 +68,6 @@ public class StateMachineService { private final TransactionTemplate transactionTemplate; - private final IdentityVerificationConfig identityVerificationConfig; - @Transactional public StateMachine processStateMachineEvent(OwnerId ownerId, String processId, OnboardingEvent event) throws IdentityVerificationException { @@ -127,25 +124,23 @@ public Message createMessage(OwnerId ownerId, String processId, public void changeMachineStatesInBatch() { final AtomicInteger countFinished = new AtomicInteger(0); try (Stream stream = identityVerificationService.streamAllIdentityVerificationsToChangeState().parallel()) { - 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); - } - }); - }); + 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); + } + }); + }); } if (countFinished.get() > 0) { logger.debug("Changed state of {} identity verifications", countFinished.get()); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/util/StateContextUtil.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/util/StateContextUtil.java index 8b45bac1e..085db8d41 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/util/StateContextUtil.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/statemachine/util/StateContextUtil.java @@ -16,11 +16,11 @@ */ package com.wultra.app.onboardingserver.statemachine.util; -import com.google.common.base.Preconditions; import com.wultra.app.onboardingserver.statemachine.consts.ExtendedStateVariable; import com.wultra.app.onboardingserver.statemachine.enums.OnboardingEvent; import com.wultra.app.onboardingserver.statemachine.enums.OnboardingState; import io.getlime.core.rest.model.base.response.Response; +import org.apache.commons.lang3.Validate; import org.springframework.http.HttpStatus; import org.springframework.statemachine.StateContext; @@ -38,7 +38,7 @@ private StateContextUtil() { } public static void setResponseOk(final StateContext context, final Response response) { - Preconditions.checkArgument( + Validate.isTrue( !context.getStateMachine().hasStateMachineError(), String.format("Found state machine error in %s, when expected ok", context) ); diff --git a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/task/cleaning/CleaningService.java b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/task/cleaning/CleaningService.java index 7d32cdcd1..cbf91a4f0 100644 --- a/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/task/cleaning/CleaningService.java +++ b/enrollment-server-onboarding/src/main/java/com/wultra/app/onboardingserver/task/cleaning/CleaningService.java @@ -17,7 +17,6 @@ */ package com.wultra.app.onboardingserver.task.cleaning; -import com.google.common.collect.Lists; import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus; import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin; import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus; @@ -33,8 +32,11 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Duration; +import java.util.Collection; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * Service with cleaning functionality. @@ -120,7 +122,7 @@ public void terminateExpiredOtpCodes() { final Date createdDateExpiredOtp = DateUtil.convertExpirationToCreatedDate(otpExpiration); final List otpIds = onboardingOtpRepository.findExpiredIds(createdDateExpiredOtp); final Date now = new Date(); - for (List otpIdChunk : Lists.partition(otpIds, BATCH_SIZE)) { + for (List otpIdChunk : ListUtils.partition(otpIds, BATCH_SIZE)) { terminateAndAuditOtps(otpIdChunk, now); } } @@ -139,7 +141,7 @@ public void terminateExpiredProcesses() { return; } logger.info("Terminating {} expired processes", ids.size()); - for (List idsChunk : Lists.partition(ids, BATCH_SIZE)) { + for (List idsChunk : ListUtils.partition(ids, BATCH_SIZE)) { terminateAndAuditProcesses(idsChunk, now, OnboardingProcessEntity.ERROR_PROCESS_EXPIRED_ONBOARDING, ErrorOrigin.PROCESS_LIMIT_CHECK); } } @@ -165,7 +167,7 @@ public void terminateExpiredDocumentVerifications() { } final Date now = new Date(); - for (List idsChunk : Lists.partition(ids, BATCH_SIZE)) { + for (List idsChunk : ListUtils.partition(ids, BATCH_SIZE)) { logger.info("Terminating {} expired document verifications", idsChunk.size()); terminateAndAuditDocuments(idsChunk, now, ERROR_MESSAGE_DOCUMENT_VERIFICATION_EXPIRED, ErrorOrigin.PROCESS_LIMIT_CHECK); } @@ -184,7 +186,7 @@ public void terminateExpiredIdentityVerifications() { final Date now = new Date(); final ErrorOrigin errorOrigin = ErrorOrigin.PROCESS_LIMIT_CHECK; - for (List idsChunk : Lists.partition(ids, BATCH_SIZE)) { + for (List idsChunk : ListUtils.partition(ids, BATCH_SIZE)) { logger.info("Terminating {} expired identity verifications", idsChunk.size()); terminateAndAuditIdentityVerifications(idsChunk, now, OnboardingProcessEntity.ERROR_PROCESS_EXPIRED_ONBOARDING, errorOrigin); } @@ -207,7 +209,7 @@ private void terminateProcessesAndRelatedEntities(final List processIds, final Date now = new Date(); final ErrorOrigin errorOrigin = ErrorOrigin.PROCESS_LIMIT_CHECK; - for (List processIdChunk : Lists.partition(processIds, BATCH_SIZE)) { + for (List processIdChunk : ListUtils.partition(processIds, BATCH_SIZE)) { logger.info("Terminating {} processes", processIdChunk.size()); terminateAndAuditProcesses(processIdChunk, now, errorDetail, errorOrigin); @@ -248,4 +250,21 @@ private void terminateAndAuditDocuments(final List documentIds, final Da documentVerificationRepository.findById(documentId).ifPresent(document -> auditService.audit(document, "Expired Document verification for user: {}, {}", document.getIdentityVerification().getUserId(), errorDetail))); } + + protected static final class ListUtils { + + private ListUtils() { + throw new IllegalStateException("Utility class"); + } + + public static Collection> partition(final List source, final int partitionSize) { + if (source.size() <= partitionSize) { + return List.of(source); + } + return IntStream.range(0, source.size()) + .boxed() + .collect(Collectors.groupingBy(partition -> (partition / partitionSize), Collectors.mapping(source::get, Collectors.toList()))) + .values(); + } + } } diff --git a/enrollment-server-onboarding/src/main/resources/application.properties b/enrollment-server-onboarding/src/main/resources/application.properties index 09f3c82f0..e937a6aec 100644 --- a/enrollment-server-onboarding/src/main/resources/application.properties +++ b/enrollment-server-onboarding/src/main/resources/application.properties @@ -28,7 +28,6 @@ banner.application.version=@project.version@ spring.datasource.url=jdbc:postgresql://localhost:5432/powerauth spring.datasource.username=powerauth spring.datasource.password= -spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.hikari.auto-commit=false spring.jpa.properties.hibernate.connection.characterEncoding=utf8 spring.jpa.properties.hibernate.connection.useUnicode=true @@ -37,7 +36,6 @@ spring.jpa.properties.hibernate.connection.useUnicode=true #spring.datasource.url=jdbc:oracle:thin:@//127.0.0.1:1521/powerauth #spring.datasource.username=powerauth #spring.datasource.password= -#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver # Hibernate Configuration spring.jpa.hibernate.ddl-auto=none @@ -90,6 +88,7 @@ enrollment-server-onboarding.onboarding-process.max-error-score=15 # Client Evaluation Configuration enrollment-server-onboarding.client-evaluation.max-failed-attempts=5 +enrollment-server-onboarding.client-evaluation.include-extracted-data=false # Identity Verification Configuration enrollment-server-onboarding.identity-verification.enabled=false @@ -209,6 +208,7 @@ powerauth.service.correlation-header.value.validation-regexp=[a-zA-Z0-9\\-]{8,10 #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.tracing.sampling.probability=1.0 #management.endpoint.metrics.enabled=true #management.endpoints.web.exposure.include=health, prometheus #management.endpoint.prometheus.enabled=true diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationServiceTest.java index 28caa5d02..55dd8ec5a 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationServiceTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/ClientEvaluationServiceTest.java @@ -19,7 +19,9 @@ import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus; import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin; +import com.wultra.app.enrollmentserver.model.integration.DocumentSubmitResult; import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.onboardingserver.common.database.entity.DocumentResultEntity; import com.wultra.app.onboardingserver.common.database.entity.DocumentVerificationEntity; import com.wultra.app.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.service.AuditService; @@ -34,6 +36,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; import java.util.Set; import java.util.UUID; @@ -71,12 +74,15 @@ class ClientEvaluationServiceTest { void testProcessClientEvaluation_successful() throws Exception { when(identityVerificationConfig.getClientEvaluationMaxFailedAttempts()) .thenReturn(1); + when(identityVerificationConfig.isSendingExtractedDataEnabled()) + .thenReturn(true); final EvaluateClientRequest evaluateClientRequest = EvaluateClientRequest.builder() .processId("p1") .userId("u1") .identityVerificationId("i1") .verificationId("v1") + .extractedData(List.of("d1_data")) .build(); final EvaluateClientResponse evaluateClientResponse = EvaluateClientResponse.builder() .accepted(true) @@ -90,8 +96,8 @@ void testProcessClientEvaluation_successful() throws Exception { identityVerification.setUserId("u1"); identityVerification.setPhase(CLIENT_EVALUATION); identityVerification.setDocumentVerifications(Set.of( - createDocumentVerification("d1", DocumentStatus.ACCEPTED, "v1"), - createDocumentVerification("d2", DocumentStatus.ACCEPTED, "v1"), + createDocumentVerificationWithResults("d1", DocumentStatus.ACCEPTED, "v1", "d1_data"), + createDocumentVerificationWithResults("d2", DocumentStatus.ACCEPTED, "v1", DocumentSubmitResult.NO_DATA_EXTRACTED), createDocumentVerification("d3", DocumentStatus.DISPOSED, "v2"))); final OwnerId ownerId = new OwnerId(); @@ -166,4 +172,13 @@ private static DocumentVerificationEntity createDocumentVerification(final Strin documentVerification.setUsedForVerification(true); return documentVerification; } + + private static DocumentVerificationEntity createDocumentVerificationWithResults(final String id, final DocumentStatus status, final String verificationId, final String extractedData) { + final DocumentResultEntity documentResult = new DocumentResultEntity(); + documentResult.setExtractedData(extractedData); + + final DocumentVerificationEntity documentVerification = createDocumentVerification(id, status, verificationId); + documentVerification.setResults(Set.of(documentResult)); + return documentVerification; + } } diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationServiceTest.java index 02fda8c0c..a9fb95c73 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationServiceTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/impl/service/IdentityVerificationServiceTest.java @@ -17,9 +17,13 @@ */ package com.wultra.app.onboardingserver.impl.service; +import com.wultra.app.enrollmentserver.model.enumeration.DocumentStatus; import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin; import com.wultra.app.enrollmentserver.model.integration.OwnerId; +import com.wultra.app.onboardingserver.api.provider.DocumentVerificationProvider; 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.onboardingserver.common.database.entity.IdentityVerificationEntity; import com.wultra.app.onboardingserver.common.service.AuditService; import org.junit.jupiter.api.Test; @@ -28,6 +32,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.util.CollectionUtils; + +import java.util.List; +import java.util.Set; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.COMPLETED; import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationPhase.OTP_VERIFICATION; @@ -35,6 +43,7 @@ import static com.wultra.app.enrollmentserver.model.enumeration.IdentityVerificationStatus.FAILED; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; /** @@ -54,6 +63,9 @@ class IdentityVerificationServiceTest { @Mock private IdentityVerificationPrecompleteCheck identityVerificationPrecompleteCheck; + @Mock + private DocumentVerificationProvider documentVerificationProvider; + @InjectMocks private IdentityVerificationService tested; @@ -96,4 +108,49 @@ void testProcessDocumentVerificationResult_invalidPrecompleteGuard() throws Exce assertThat(savedIdentityVerification.getErrorDetail(), equalTo("documentVerificationFailed")); assertThat(savedIdentityVerification.getErrorOrigin(), equalTo(ErrorOrigin.FINAL_VALIDATION)); } + + @Test + void testCreateDocsMetadata_hideRejectedErrorDetail() { + final DocumentVerificationEntity doc = new DocumentVerificationEntity(); + doc.setStatus(DocumentStatus.REJECTED); + doc.setErrorDetail("Hide specific error occurred."); + + final List errors = tested.createDocsMetadata(List.of(doc)).get(0).getErrors(); + assertHidden(errors); + } + + @Test + void testCreateDocsMetadata_hideRejectedRejectReason() { + final DocumentVerificationEntity doc = new DocumentVerificationEntity(); + doc.setStatus(DocumentStatus.REJECTED); + doc.setRejectReason("Hide specific rejection reason."); + + final List errors = tested.createDocsMetadata(List.of(doc)).get(0).getErrors(); + assertHidden(errors); + } + + @Test + void testCreateDocsMetadata_hideFailedErrorDetail() { + final DocumentVerificationEntity doc = new DocumentVerificationEntity(); + doc.setStatus(DocumentStatus.FAILED); + doc.setErrorDetail("Hide some error occurred."); + + final List errors = tested.createDocsMetadata(List.of(doc)).get(0).getErrors(); + assertHidden(errors); + } + + @Test + void testCreateDocsMetadata_accepted() { + final DocumentVerificationEntity doc = new DocumentVerificationEntity(); + doc.setStatus(DocumentStatus.ACCEPTED); + + final List errors = tested.createDocsMetadata(List.of(doc)).get(0).getErrors(); + assertTrue(CollectionUtils.isEmpty(errors)); + } + + private static void assertHidden(final List errors) { + assertEquals(1, errors.size()); + assertEquals("Error verifying the document.", errors.get(0)); + } + } diff --git a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/task/cleaning/CleaningServiceTest.java b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/task/cleaning/CleaningServiceTest.java index 12ccf3faf..8022720c0 100644 --- a/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/task/cleaning/CleaningServiceTest.java +++ b/enrollment-server-onboarding/src/test/java/com/wultra/app/onboardingserver/task/cleaning/CleaningServiceTest.java @@ -28,6 +28,11 @@ import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + import static com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin.PROCESS_LIMIT_CHECK; import static org.junit.jupiter.api.Assertions.*; @@ -227,6 +232,30 @@ void testTerminateExpiredProcessActivations() { assertEquals(PROCESS_LIMIT_CHECK, documentVerification.getErrorOrigin()); } + @Test + void testPartition() { + final List source = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h"); + + final Collection> result = CleaningService.ListUtils.partition(source, 3); + + assertEquals(3, result.size()); + + final Iterator> iterator = result.iterator(); + assertEquals(List.of("a", "b", "c"), iterator.next()); + assertEquals(List.of("d", "e", "f"), iterator.next()); + assertEquals(List.of("g", "h"), iterator.next()); + } + + @Test + void testPartition_tooSmall() { + final List source = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h"); + + final Collection> result = CleaningService.ListUtils.partition(source, 10); + + assertEquals(1, result.size()); + assertEquals(List.of("a", "b", "c", "d", "e", "f", "g", "h"), result.iterator().next()); + } + private void assertStatus(final String id, final DocumentStatus status) { final DocumentVerificationEntity documentVerification = fetchDocumentVerification(id); assertEquals(status, documentVerification.getStatus(), "status of " + id); diff --git a/enrollment-server-onboarding/src/test/resources/application-test.properties b/enrollment-server-onboarding/src/test/resources/application-test.properties index 5dff6a04e..5ee3babae 100644 --- a/enrollment-server-onboarding/src/test/resources/application-test.properties +++ b/enrollment-server-onboarding/src/test/resources/application-test.properties @@ -18,7 +18,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.hibernate.ddl-auto=create spring.liquibase.enabled=false diff --git a/enrollment-server/pom.xml b/enrollment-server/pom.xml index bea22f846..b49671061 100644 --- a/enrollment-server/pom.xml +++ b/enrollment-server/pom.xml @@ -30,7 +30,7 @@ com.wultra.security enrollment-server-parent - 1.6.0 + 1.7.0 @@ -98,6 +98,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + jakarta.servlet @@ -123,6 +127,16 @@ micrometer-registry-prometheus + + io.projectreactor + reactor-core-micrometer + + + + io.micrometer + micrometer-tracing-bridge-otel + + org.springframework.boot diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/OpenApiConfiguration.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/OpenApiConfiguration.java index 69c69a1aa..0a5622140 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/OpenApiConfiguration.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/OpenApiConfiguration.java @@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.info.License; import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -49,9 +50,11 @@ ) public class OpenApiConfiguration { + private static final String PACKAGE_ADMIN = "com.wultra.app.enrollmentserver.controller.api.admin"; + @Bean public GroupedOpenApi defaultApiGroup() { - String[] packages = { + final String[] packages = { "io.getlime.security.powerauth", "com.wultra.app.enrollmentserver.controller.api" }; @@ -59,6 +62,16 @@ public GroupedOpenApi defaultApiGroup() { return GroupedOpenApi.builder() .group("enrollment-server") .packagesToScan(packages) + .packagesToExclude(PACKAGE_ADMIN) + .build(); + } + + @Bean + @ConditionalOnProperty(value = "enrollment-server.admin.enabled", havingValue = "true") + public GroupedOpenApi adminApiGroup() { + return GroupedOpenApi.builder() + .group("enrollment-server-admin") + .packagesToScan(PACKAGE_ADMIN) .build(); } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/SecurityConfig.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/SecurityConfig.java index 51cdc35f8..2b92f3d2d 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/SecurityConfig.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/configuration/SecurityConfig.java @@ -18,12 +18,26 @@ package com.wultra.app.enrollmentserver.configuration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; + +import static org.springframework.security.config.Customizer.withDefaults; /** * Spring Security configuration. @@ -32,14 +46,72 @@ */ @Configuration @EnableWebSecurity +@Slf4j public class SecurityConfig { + @Value("${enrollment-server.auth-type}") + private AuthType authType; + @Bean public SecurityFilterChain filterChain(final HttpSecurity http) throws Exception { + Assert.state(authType != null, "No authentication type configured."); + + if (authType == AuthType.NONE) { + logger.info("No authentication."); + http.httpBasic(AbstractHttpConfigurer::disable); + } else { + http.authorizeHttpRequests(authorize -> authorize + .requestMatchers(new AntPathRequestMatcher("/api/admin/**")).authenticated() + .anyRequest().permitAll()); + } + + if (authType == AuthType.BASIC_HTTP) { + logger.info("Initializing HTTP basic authentication."); + http.httpBasic(withDefaults()); + } else if (authType == AuthType.OIDC) { + logger.info("Initializing OIDC authentication."); + http.oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); + } + return http - .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) .build(); } + @ConditionalOnProperty(value = "enrollment-server.auth-type", havingValue = "BASIC_HTTP" ) + @Bean + public UserDetailsService userDetailsService(final SecurityProperties securityProperties) { + final String username = securityProperties.getUser().getName(); + Assert.hasLength(username, "Username must not be blank."); + logger.info("Initializing user detail service for: {}", username); + final UserDetails user = User.withUsername(username) + .password(securityProperties.getUser().getPassword()) + .build(); + return new InMemoryUserDetailsManager(user); + } + + @ConditionalOnProperty(value = "enrollment-server.auth-type", havingValue = "BASIC_HTTP" ) + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + + enum AuthType { + + /** + * Authentication is turned off. + */ + NONE, + + /** + * Basic HTTP authentication. + */ + BASIC_HTTP, + + /** + * OpenID Connect. + */ + OIDC + } + } 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 1ef71aeb8..61927ad47 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 @@ -21,12 +21,15 @@ import com.wultra.app.enrollmentserver.errorhandling.MobileTokenAuthException; import com.wultra.app.enrollmentserver.errorhandling.MobileTokenConfigurationException; import com.wultra.app.enrollmentserver.errorhandling.MobileTokenException; +import com.wultra.app.enrollmentserver.errorhandling.RemoteCommunicationException; import com.wultra.app.enrollmentserver.impl.service.MobileTokenService; import com.wultra.app.enrollmentserver.impl.service.OperationApproveParameterObject; 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.client.model.error.PowerAuthError; 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.request.OperationApproveRequest; import com.wultra.security.powerauth.lib.mtoken.model.request.OperationDetailRequest; import com.wultra.security.powerauth.lib.mtoken.model.request.OperationRejectRequest; @@ -68,6 +71,13 @@ @RequestMapping("api/auth/token/app") public class MobileTokenController { + private static final String APPLICATION_NOT_FOUND = "ERR0015"; + private static final String INVALID_REQUEST = "ERR0024"; + private static final String OPERATION_NOT_FOUND = "ERR0034"; + private static final String OPERATION_INVALID_STATE = "ERR0036"; + private static final String OPERATION_APPROVE_FAILURE = "ERR0037"; + private static final String OPERATION_REJECT_FAILURE = "ERR0038"; + private static final Logger logger = LoggerFactory.getLogger(MobileTokenController.class); // Disallowed flags contain onboarding flags used before onboarding process is finished @@ -101,7 +111,7 @@ public MobileTokenController(MobileTokenService mobileTokenService) { PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE, PowerAuthSignatureTypes.POSSESSION_KNOWLEDGE_BIOMETRY }) - public ObjectResponse operationList(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException { + public ObjectResponse operationList(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException { try { if (auth != null) { final String userId = auth.getUserId(); @@ -115,8 +125,24 @@ public ObjectResponse operationList(@Parameter(hidden = t throw new MobileTokenAuthException(); } } catch (PowerAuthClientException e) { - logger.error("Unable to call upstream service.", e); - throw new MobileTokenAuthException(); + final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING"); + switch (errorCode) { + case APPLICATION_NOT_FOUND -> { + logger.info("Application ID: {} not found: {}", auth.getApplicationId(), e.getMessage()); + logger.debug("Application ID: {} not found.", auth.getApplicationId(), e); + throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier."); + } + case INVALID_REQUEST -> { + logger.info("Request validation error: {}", e.getMessage()); + logger.debug("Request validation error.", e); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage())); + } + default -> { + logger.warn("Calling PowerAuth service failed: {}", e.getMessage()); + logger.debug("Calling PowerAuth service failed.", e); + throw new RemoteCommunicationException("Unable to call upstream service."); + } + } } } @@ -138,7 +164,7 @@ public ObjectResponse operationList(@Parameter(hidden = t }) public ObjectResponse getOperationDetail(@RequestBody ObjectRequest request, @Parameter(hidden = true) PowerAuthApiAuthentication auth, - @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException { + @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException { try { if (auth != null) { final String operationId = request.getRequestObject().getId(); @@ -151,8 +177,24 @@ public ObjectResponse getOperationDetail(@RequestBody ObjectRequest { + logger.info("Operation ID: {} not found: {}", request.getRequestObject().getId(), e.getMessage()); + logger.debug("Operation ID: {} not found.", request.getRequestObject().getId(), e); + throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "No operation was found with the provided identifier."); + } + case INVALID_REQUEST -> { + logger.info("Request validation error: {}", e.getMessage()); + logger.debug("Request validation error.", e); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage())); + } + default -> { + logger.warn("Calling PowerAuth service failed: {}", e.getMessage()); + logger.debug("Calling PowerAuth service failed.", e); + throw new RemoteCommunicationException("Unable to call upstream service."); + } + } } } @@ -174,7 +216,7 @@ public ObjectResponse getOperationDetail(@RequestBody ObjectRequest claimOperation(@RequestBody ObjectRequest request, @Parameter(hidden = true) PowerAuthApiAuthentication auth, - @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException { + @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException { try { if (auth != null) { final String operationId = request.getRequestObject().getId(); @@ -187,8 +229,24 @@ public ObjectResponse claimOperation(@RequestBody ObjectRequest { + logger.info("Operation ID: {} not found: {}", request.getRequestObject().getId(), e.getMessage()); + logger.debug("Operation ID: {} not found.", request.getRequestObject().getId(), e); + throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "No operation was found with the provided identifier."); + } + case INVALID_REQUEST -> { + logger.info("Request validation error: {}", e.getMessage()); + logger.debug("Request validation error.", e); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage())); + } + default -> { + logger.warn("Calling PowerAuth service failed: {}", e.getMessage()); + logger.debug("Calling PowerAuth service failed.", e); + throw new RemoteCommunicationException("Unable to call upstream service."); + } + } } } @@ -206,7 +264,7 @@ public ObjectResponse claimOperation(@RequestBody ObjectRequest operationListAll(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException { + public ObjectResponse operationListAll(@Parameter(hidden = true) PowerAuthApiAuthentication auth, @Parameter(hidden = true) Locale locale) throws MobileTokenException, MobileTokenConfigurationException, RemoteCommunicationException { try { if (auth != null) { final String userId = auth.getUserId(); @@ -219,8 +277,24 @@ public ObjectResponse operationListAll(@Parameter(hidden throw new MobileTokenAuthException(); } } catch (PowerAuthClientException e) { - logger.error("Unable to call upstream service.", e); - throw new MobileTokenAuthException(); + final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING"); + switch (errorCode) { + case APPLICATION_NOT_FOUND -> { + logger.info("Application ID: {} not found: {}", auth.getApplicationId(), e.getMessage()); + logger.debug("Application ID: {} not found.", auth.getApplicationId(), e); + throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier."); + } + case INVALID_REQUEST -> { + logger.info("Request validation error: {}", e.getMessage()); + logger.debug("Request validation error.", e); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage())); + } + default -> { + logger.warn("Calling PowerAuth service failed: {}", e.getMessage()); + logger.debug("Calling PowerAuth service failed.", e); + throw new RemoteCommunicationException("Unable to call upstream service."); + } + } } } @@ -242,7 +316,7 @@ public ObjectResponse operationListAll(@Parameter(hidden public Response operationApprove( @RequestBody ObjectRequest request, @Parameter(hidden = true) PowerAuthApiAuthentication auth, - HttpServletRequest servletRequest) throws MobileTokenException { + HttpServletRequest servletRequest) throws MobileTokenException, RemoteCommunicationException { try { final OperationApproveRequest requestObject = request.getRequestObject(); @@ -290,8 +364,30 @@ public Response operationApprove( throw new MobileTokenAuthException(); } } catch (PowerAuthClientException e) { - logger.error("Unable to call upstream service.", e); - throw new MobileTokenAuthException(); + final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING"); + switch (errorCode) { + case APPLICATION_NOT_FOUND -> { + final String applicationId = auth != null ? auth.getApplicationId() : null; + logger.info("Application ID: {} not found: {}", applicationId, e.getMessage()); + logger.debug("Application ID: {} not found.", applicationId, e); + throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier."); + } + case OPERATION_NOT_FOUND, OPERATION_APPROVE_FAILURE, OPERATION_INVALID_STATE -> { + logger.info("Operation ID: {} not found or is in unexpected state: {}", request.getRequestObject().getId(), e.getMessage()); + logger.debug("Operation ID: {} not found or is in unexpected state.", request.getRequestObject().getId(), e); + throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "Operation not found or is in an unexpected state."); + } + case INVALID_REQUEST -> { + logger.info("Request validation error: {}", e.getMessage()); + logger.debug("Request validation error.", e); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage())); + } + default -> { + logger.warn("Calling PowerAuth service failed: {}", e.getMessage()); + logger.debug("Calling PowerAuth service failed.", e); + throw new RemoteCommunicationException("Unable to call upstream service."); + } + } } } @@ -320,7 +416,7 @@ private static String fetchProximityCheckOtp(OperationApproveRequest requestObje public Response operationReject( @RequestBody ObjectRequest request, @Parameter(hidden = true) PowerAuthApiAuthentication auth, - HttpServletRequest servletRequest) throws MobileTokenException { + HttpServletRequest servletRequest) throws MobileTokenException, RemoteCommunicationException { try { final OperationRejectRequest requestObject = request.getRequestObject(); @@ -342,8 +438,29 @@ public Response operationReject( throw new MobileTokenAuthException(); } } catch (PowerAuthClientException e) { - logger.error("Unable to call upstream service.", e); - throw new MobileTokenAuthException(); + final String errorCode = e.getPowerAuthError().map(PowerAuthError::getCode).orElse("ERROR_CODE_MISSING"); + switch (errorCode) { + case APPLICATION_NOT_FOUND -> { + logger.info("Application ID: {} not found: {}", auth.getApplicationId(), e.getMessage()); + logger.debug("Application ID: {} not found.", auth.getApplicationId(), e); + throw new MobileTokenException(ErrorCode.INVALID_APPLICATION, "No application was found with the provided identifier: %s".formatted(auth.getApplicationId())); + } + case OPERATION_NOT_FOUND, OPERATION_REJECT_FAILURE -> { + logger.info("Operation ID: {} not found or is in unexpected state: {}", request.getRequestObject().getId(), e.getMessage()); + logger.debug("Operation ID: {} not found or is in unexpected state.", request.getRequestObject().getId(), e); + throw new MobileTokenException(ErrorCode.INVALID_OPERATION, "Operation not found or is in an unexpected state"); + } + case INVALID_REQUEST -> { + logger.info("Request validation error: {}", e.getMessage()); + logger.debug("Request validation error.", e); + throw new MobileTokenException(ErrorCode.INVALID_REQUEST, "Request validation error: %s".formatted(e.getMessage())); + } + default -> { + logger.warn("Calling PowerAuth service failed: {}", e.getMessage()); + logger.debug("Calling PowerAuth service failed.", e); + throw new RemoteCommunicationException("Unable to call upstream service."); + } + } } } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/admin/AdminController.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/admin/AdminController.java new file mode 100644 index 000000000..b1a9e8839 --- /dev/null +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/controller/api/admin/AdminController.java @@ -0,0 +1,89 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2024 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.controller.api.admin; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wultra.app.enrollmentserver.api.model.enrollment.response.TemplateListResponse; +import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity; +import com.wultra.app.enrollmentserver.impl.service.OperationTemplateService; +import io.getlime.core.rest.model.base.response.ObjectResponse; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Admin controller. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@ConditionalOnProperty(value = "enrollment-server.admin.enabled", havingValue = "true") +@RestController +@RequestMapping(value = "api/admin") +@AllArgsConstructor +@Slf4j +public class AdminController { + + private final OperationTemplateService operationTemplateService; + + private final ObjectMapper objectMapper; + + @GetMapping("/template") + public ObjectResponse templates() { + logger.debug("Returning template list."); + final TemplateListResponse response = new TemplateListResponse(); + response.addAll(convert(operationTemplateService.findAll())); + return new ObjectResponse<>(response); + } + + private List convert(final List source) { + return source.stream() + .map(this::convert) + .toList(); + } + + private TemplateListResponse.TemplateDetail convert(final OperationTemplateEntity source) { + return TemplateListResponse.TemplateDetail.builder() + .name(source.getPlaceholder()) + .title(source.getTitle()) + .message(source.getMessage()) + .language(source.getLanguage()) + .attributes(convert(source.getAttributes())) + .build(); + } + + private List convert(final String source) { + if (!StringUtils.hasText(source)) { + return null; + } + + try { + return objectMapper.readValue(source, new TypeReference<>() {}); + } catch (JsonProcessingException e) { + logger.warn("Unable to convert attributes, returning an empty collection", e); + return List.of(); + } + } +} 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 40c605567..24ffa6306 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 @@ -19,7 +19,7 @@ package com.wultra.app.enrollmentserver.database; import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.ListCrudRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @@ -30,7 +30,7 @@ * @author Petr Dvorak, petr@wultra.com */ @Repository -public interface OperationTemplateRepository extends CrudRepository { +public interface OperationTemplateRepository extends ListCrudRepository { /** * Find an operation template by the given language and operation type. 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 80ac42a4c..e85b95220 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 @@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.resource.NoResourceFoundException; /** * Exception handler for RESTful API issues. @@ -49,6 +50,19 @@ public class DefaultExceptionHandler { return new ErrorResponse("ERROR_GENERIC", "Unknown error occurred while processing request."); } + /** + * Handling of remote communication exception. + * + * @param ex Exception. + * @return Response with error details. + */ + @ExceptionHandler(RemoteCommunicationException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public @ResponseBody ErrorResponse handleRemoteExceptionException(RemoteCommunicationException ex) { + logger.warn("Communication with remote system failed", ex); + return new ErrorResponse("REMOTE_COMMUNICATION_ERROR", "Communication with remote system failed."); + } + /** * Exception handler for invalid request exception. * @param ex Exception. @@ -145,4 +159,17 @@ public class DefaultExceptionHandler { return new ErrorResponse("INBOX_FAILED", "Unable to process inbox request."); } + /** + * Exception handler for no resource found. + * + * @param e Exception. + * @return Response with error details. + */ + @ExceptionHandler(NoResourceFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public @ResponseBody ErrorResponse handleNoResourceFoundException(final NoResourceFoundException e) { + logger.warn("Error occurred when calling an API: {}", e.getMessage()); + logger.debug("Exception detail: ", e); + return new ErrorResponse("ERROR_NOT_FOUND", "Resource not found."); + } } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/RemoteCommunicationException.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/RemoteCommunicationException.java new file mode 100644 index 000000000..34528c1de --- /dev/null +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/errorhandling/RemoteCommunicationException.java @@ -0,0 +1,40 @@ +/* + * PowerAuth Enrollment Server + * Copyright (C) 2024 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.errorhandling; + +import java.io.Serial; + +/** + * Exception thrown in case of an error during communication with remote system. + * + * @author Jan Pesek, jan.pesek@wultra.com + */ +public class RemoteCommunicationException extends Exception { + + @Serial + private static final long serialVersionUID = -2565764734609472778L; + + public RemoteCommunicationException(String message) { + super(message); + } + + public RemoteCommunicationException(String message, Throwable cause) { + super(message, cause); + } + +} 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 9aafce226..d3a93778e 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 @@ -25,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.List; import java.util.Optional; /** @@ -59,6 +60,15 @@ public Optional findTemplate(@NotNull String operationT findTemplateFallback(operationType, language)); } + /** + * Find all templates. + * + * @return templates + */ + public List findAll() { + return operationTemplateRepository.findAll(); + } + private Optional findTemplateFallback(final String operationType, final String language) { if (!DEFAULT_LANGUAGE.equals(language)) { logger.debug("Trying fallback to EN locale for operationType={}", operationType); diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/PushRegistrationService.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/PushRegistrationService.java index ecaf31ceb..2ba048a4a 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/PushRegistrationService.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/service/PushRegistrationService.java @@ -72,17 +72,11 @@ public Response registerDevice( throw new InvalidRequestObjectException(); } - // Get the values from the request - final String platform = requestObject.getPlatform(); + final MobilePlatform platform = convert(requestObject.getPlatform()); final String token = requestObject.getToken(); - // Register the device and return response - MobilePlatform mobilePlatform = MobilePlatform.Android; - if ("ios".equalsIgnoreCase(platform)) { - mobilePlatform = MobilePlatform.iOS; - } try { - final boolean result = client.createDevice(applicationId, token, mobilePlatform, activationId); + final boolean result = client.createDevice(applicationId, token, platform, activationId); if (result) { logger.info("Push registration succeeded, user ID: {}", userId); return new Response(); @@ -96,4 +90,12 @@ public Response registerDevice( } } + private static MobilePlatform convert(final PushRegisterRequest.Platform source) { + return switch (source) { + case IOS -> MobilePlatform.IOS; + case ANDROID -> MobilePlatform.ANDROID; + case HUAWEI -> MobilePlatform.HUAWEI; + }; + } + } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/util/ConditionalOnPropertyNotEmpty.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/util/ConditionalOnPropertyNotEmpty.java index 89a6e8771..e96aa8c48 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/util/ConditionalOnPropertyNotEmpty.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/impl/util/ConditionalOnPropertyNotEmpty.java @@ -18,7 +18,7 @@ package com.wultra.app.enrollmentserver.impl.util; -import com.google.common.base.Strings; +import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Condition; import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; @@ -52,7 +52,7 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) Map attrs = metadata.getAnnotationAttributes(ConditionalOnPropertyNotEmpty.class.getName()); String propertyName = (String) Objects.requireNonNull(attrs).get("value"); String val = context.getEnvironment().getProperty(propertyName); - return !Strings.nullToEmpty(val).trim().isEmpty(); + return StringUtils.isNotBlank(val); } } diff --git a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/model/validator/PushRegisterRequestValidator.java b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/model/validator/PushRegisterRequestValidator.java index aa0fe01ce..e0a731f9d 100644 --- a/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/model/validator/PushRegisterRequestValidator.java +++ b/enrollment-server/src/main/java/com/wultra/app/enrollmentserver/model/validator/PushRegisterRequestValidator.java @@ -42,11 +42,8 @@ public static String validate(PushRegisterRequest request) { } // Validate mobile platform - final String platform = request.getPlatform(); - if (StringUtils.isBlank(platform)) { + if (request.getPlatform() == null) { return "No mobile platform was provided when registering for push messages."; - } else if (!"ios".equalsIgnoreCase(platform) && !"android".equalsIgnoreCase(platform)) { // must be iOS or Android - return "Unknown mobile platform was provided when registering for push messages."; } // Validate push token @@ -56,7 +53,6 @@ public static String validate(PushRegisterRequest request) { } return null; - } } diff --git a/enrollment-server/src/main/resources/application-dev.properties b/enrollment-server/src/main/resources/application-dev.properties index 524c75a0d..165c749e1 100644 --- a/enrollment-server/src/main/resources/application-dev.properties +++ b/enrollment-server/src/main/resources/application-dev.properties @@ -1,3 +1,9 @@ # Liquibase spring.liquibase.enabled=true spring.liquibase.change-log=classpath:db/changelog/db.changelog-module.xml + +enrollment-server.admin.enabled=true +enrollment-server.auth-type=BASIC_HTTP + +spring.security.user.name=admin +spring.security.user.password={bcrypt}$2a$10$Im45aSJeMpove4pF8/ypB.ufkITjfjpFvby9AMkvy.hrOVixkfkxq diff --git a/enrollment-server/src/main/resources/application.properties b/enrollment-server/src/main/resources/application.properties index a0da944e3..195eaa2bc 100644 --- a/enrollment-server/src/main/resources/application.properties +++ b/enrollment-server/src/main/resources/application.properties @@ -28,7 +28,6 @@ banner.application.version=@project.version@ spring.datasource.url=jdbc:postgresql://localhost:5432/powerauth spring.datasource.username=powerauth spring.datasource.password= -spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.hikari.auto-commit=false spring.jpa.properties.hibernate.connection.characterEncoding=utf8 spring.jpa.properties.hibernate.connection.useUnicode=true @@ -37,7 +36,6 @@ spring.jpa.properties.hibernate.connection.useUnicode=true #spring.datasource.url=jdbc:oracle:thin:@//127.0.0.1:1521/powerauth #spring.datasource.username=powerauth #spring.datasource.password= -#spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver # Hibernate Configuration spring.jpa.hibernate.ddl-auto=none @@ -70,6 +68,16 @@ powerauth.service.security.clientSecret= enrollment-server.mtoken.enabled=true enrollment-server.inbox.enabled=true enrollment-server.activation-spawn.enabled=false +enrollment-server.admin.enabled=false +enrollment-server.auth-type=NONE + +# Basic HTTP Settings +spring.security.user.name= +spring.security.user.password= + +# OIDC Settings +spring.security.oauth2.resource-server.jwt.issuer-uri= +spring.security.oauth2.resource-server.jwt.audiences= # User-info configuration # enrollment-server.user-info.provider= @@ -90,7 +98,10 @@ powerauth.service.correlation-header.value.validation-regexp=[a-zA-Z0-9\\-]{8,10 #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.tracing.sampling.probability=1.0 #management.endpoint.metrics.enabled=true #management.endpoints.web.exposure.include=health, prometheus #management.endpoint.prometheus.enabled=true #management.prometheus.metrics.export.enabled=true + +spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration 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 c17ce6611..16e32c579 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 @@ -18,7 +18,6 @@ package com.wultra.app.enrollmentserver.impl.service.converter; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.ImmutableMap; import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity; import com.wultra.app.enrollmentserver.errorhandling.MobileTokenConfigurationException; import com.wultra.security.powerauth.client.model.enumeration.OperationStatus; @@ -362,27 +361,27 @@ void testConvertUiPostApprovalGenericMessageWithSubstitutedDangerousChars() thro @Test void testConvertAttributes() throws Exception { final OperationDetailResponse operationDetail = createOperationDetailResponse(); - operationDetail.setParameters(ImmutableMap.builder() - .put("amount", "13.7") - .put("currency", "EUR") - .put("iban", "AT483200000012345864") - .put("note", "Remember me") - .put("headingLevel", "3") - .put("thumbnailUrl", "https://example.com/123_thumb.jpeg") - .put("originalUrl", "https://example.com/123.jpeg") - .put("sourceAmount", "1.26") - .put("sourceCurrency", "ETH") - .put("targetAmount", "1710.98") - .put("targetCurrency", "USD") - .put("dynamic", "true") - .put("partyLogoUrl", "https://example.com/img/logo/logo.svg") - .put("partyName", "Example Ltd.") - .put("partyDescription", "Find out more about Example...") - .put("partyUrl", "https://example.com/hello") - .put("alertType", "WARNING") - .put("alertTitle", "Insufficient Balance") - .put("alertMessage", "You have only $1.00 on your account with number 238400856/0300.") - .build()); + operationDetail.setParameters(Map.ofEntries( + Map.entry("amount", "13.7"), + Map.entry("currency", "EUR"), + Map.entry("iban", "AT483200000012345864"), + Map.entry("note", "Remember me"), + Map.entry("headingLevel", "3"), + Map.entry("thumbnailUrl", "https://example.com/123_thumb.jpeg"), + Map.entry("originalUrl", "https://example.com/123.jpeg"), + Map.entry("sourceAmount", "1.26"), + Map.entry("sourceCurrency", "ETH"), + Map.entry("targetAmount", "1710.98"), + Map.entry("targetCurrency", "USD"), + Map.entry("dynamic", "true"), + Map.entry("partyLogoUrl", "https://example.com/img/logo/logo.svg"), + Map.entry("partyName", "Example Ltd."), + Map.entry("partyDescription", "Find out more about Example..."), + Map.entry("partyUrl", "https://example.com/hello"), + Map.entry("alertType", "WARNING"), + Map.entry("alertTitle", "Insufficient Balance"), + Map.entry("alertMessage", "You have only $1.00 on your account with number 238400856/0300.") + )); final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); operationTemplate.setAttributes(""" @@ -518,10 +517,10 @@ void testConvertAttributes() throws Exception { @Test void testConvertAmount_notANumber() throws Exception { final OperationDetailResponse operationDetail = createOperationDetailResponse(); - operationDetail.setParameters(ImmutableMap.builder() - .put("amount", "not a number") - .put("currency", "CZK") - .build()); + operationDetail.setParameters(Map.of( + "amount", "not a number", + "currency", "CZK" + )); final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); operationTemplate.setAttributes(""" @@ -558,13 +557,13 @@ void testConvertAmount_notANumber() throws Exception { @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()); + operationDetail.setParameters(Map.of( + "sourceAmount", "source not a number", + "sourceCurrency", "EUR", + "targetAmount", "1710.98", + "targetCurrency", "USD", + "dynamic", "true" + )); final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); operationTemplate.setAttributes(""" @@ -610,13 +609,13 @@ void testConvertAmountConversion_sourceNotANumber() throws Exception { @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()); + operationDetail.setParameters(Map.of( + "sourceAmount", "1710.98", + "sourceCurrency", "USD", + "targetAmount", "target not a number", + "targetCurrency", "EUR", + "dynamic", "true" + )); final OperationTemplateEntity operationTemplate = new OperationTemplateEntity(); operationTemplate.setAttributes(""" diff --git a/mtoken-model/pom.xml b/mtoken-model/pom.xml index 880febd13..d895f43aa 100644 --- a/mtoken-model/pom.xml +++ b/mtoken-model/pom.xml @@ -26,7 +26,7 @@ com.wultra.security enrollment-server-parent - 1.6.0 + 1.7.0 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 a6ef3225a..787620eaa 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 @@ -42,6 +42,18 @@ public class ErrorCode { */ public static final String INVALID_ACTIVATION = "INVALID_ACTIVATION"; + /** + * Error code for situation when an invalid application identifier is + * attempted for operation manipulation. + */ + public static final String INVALID_APPLICATION = "INVALID_APPLICATION"; + + /** + * Error code for situation when an invalid operation identifier is + * attempted for operation manipulation. + */ + public static final String INVALID_OPERATION = "INVALID_OPERATION"; + /** * Error code for situation when signature verification fails. */ diff --git a/pom.xml b/pom.xml index 43206742c..e06f56583 100644 --- a/pom.xml +++ b/pom.xml @@ -26,13 +26,13 @@ com.wultra.security enrollment-server-parent - 1.6.0 + 1.7.0 pom org.springframework.boot spring-boot-starter-parent - 3.1.6 + 3.2.4 @@ -88,26 +88,21 @@ - 7.2.0 + 7.4.0 - 5.10.2 + 5.13.0 4.0.0 - 2.2.20 - 2.3.0 - 1.4.2 + 2.2.21 + 2.5.0 + 1.4.4 - - 3.13.0 - - 1.8.0 - 1.6.0 - 1.6.0 - 1.6.0 + 1.9.0 + 1.7.0 + 1.7.0 + 1.7.0 1.77 7.4 - - 1.4.14 @@ -316,13 +311,6 @@ org.apache.maven.plugins maven-enforcer-plugin - - - de.skuzzle.enforcer - restrict-imports-enforcer-rule - 2.4.0 - - enforce-banned-dependencies @@ -336,26 +324,12 @@ org.apache.tomcat.embed:*:*:*:compile org.bouncycastle:bcpkix-jdk15on:*:*:compile org.bouncycastle:bcprov-jdk15on:*:*:compile + com.google.guava:guava*:*:*:compile - - enforce-banned-java-imports - - enforce - - - - - - Guava depends on jsr305 but we prefer jakarta in our code - javax.annotation.** - - - -