From 6925ff7d1805e0ddeead5f9373351a32a3428782 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:35:18 +0000 Subject: [PATCH 01/23] Bump actions/cache from 3 to 4 Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/run_trivy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_trivy.yml b/.github/workflows/run_trivy.yml index 548cd92ad..18018e367 100644 --- a/.github/workflows/run_trivy.yml +++ b/.github/workflows/run_trivy.yml @@ -31,7 +31,7 @@ jobs: sha=$(gh api -H "${headers}" "${endpoint}" | jq --raw-output "${jqFilter}") echo "Trivy DB sha256:${sha}" echo "::set-output name=sha::${sha}" - - uses: actions/cache@v3 + - uses: actions/cache@v4 with: path: .trivy key: ${{ runner.os }}-trivy-db-${{ steps.trivy-db.outputs.sha }} From 0fc96c5f6c08722ba6525cb7376c1aef131c631f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:35:21 +0000 Subject: [PATCH 02/23] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build_container_develop_branch.yml | 2 +- .github/workflows/build_container_non_develop_branch.yml | 2 +- .github/workflows/build_contributer_container.yml | 2 +- .github/workflows/build_package.yml | 2 +- .github/workflows/build_pull_request.yml | 2 +- .github/workflows/run_trivy.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_container_develop_branch.yml b/.github/workflows/build_container_develop_branch.yml index 211cc9cb9..822448338 100644 --- a/.github/workflows/build_container_develop_branch.yml +++ b/.github/workflows/build_container_develop_branch.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 'Download artifact' uses: actions/github-script@v7 with: diff --git a/.github/workflows/build_container_non_develop_branch.yml b/.github/workflows/build_container_non_develop_branch.yml index a25f2ded9..189111ad5 100644 --- a/.github/workflows/build_container_non_develop_branch.yml +++ b/.github/workflows/build_container_non_develop_branch.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 'Download artifact' uses: actions/github-script@v7 with: diff --git a/.github/workflows/build_contributer_container.yml b/.github/workflows/build_contributer_container.yml index d021318c1..79b029b51 100644 --- a/.github/workflows/build_contributer_container.yml +++ b/.github/workflows/build_contributer_container.yml @@ -19,7 +19,7 @@ jobs: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: 'Download artifact' uses: actions/github-script@v7 with: diff --git a/.github/workflows/build_package.yml b/.github/workflows/build_package.yml index b17201185..ea9a1714a 100644 --- a/.github/workflows/build_package.yml +++ b/.github/workflows/build_package.yml @@ -20,7 +20,7 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 uses: actions/setup-java@v2 with: diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 9b463eb38..e9a2c3dc4 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -27,7 +27,7 @@ jobs: --health-timeout 5s --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up JDK 11 uses: actions/setup-java@v2 with: diff --git a/.github/workflows/run_trivy.yml b/.github/workflows/run_trivy.yml index 548cd92ad..af0bab2ba 100644 --- a/.github/workflows/run_trivy.yml +++ b/.github/workflows/run_trivy.yml @@ -19,7 +19,7 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - id: trivy-db name: Check trivy db sha env: From a4c84772f3f0c1d918de403976f01eaeb6dda5d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:35:24 +0000 Subject: [PATCH 03/23] Bump actions/setup-java from 2 to 4 Bumps [actions/setup-java](https://github.com/actions/setup-java) from 2 to 4. - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/setup-java dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build_package.yml | 2 +- .github/workflows/build_pull_request.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_package.yml b/.github/workflows/build_package.yml index b17201185..eb005c97e 100644 --- a/.github/workflows/build_package.yml +++ b/.github/workflows/build_package.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'adopt' diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 9b463eb38..90e102d15 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up JDK 11 - uses: actions/setup-java@v2 + uses: actions/setup-java@v4 with: java-version: '11' distribution: 'adopt' From 3c547cc2e4660ad63583a5ccedde0e3410fe1ad1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 13:35:26 +0000 Subject: [PATCH 04/23] Bump github/codeql-action from 1 to 3 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 3. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v1...v3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/run_trivy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_trivy.yml b/.github/workflows/run_trivy.yml index 548cd92ad..d5c8cf552 100644 --- a/.github/workflows/run_trivy.yml +++ b/.github/workflows/run_trivy.yml @@ -49,6 +49,6 @@ jobs: - name: Fix .trivy permissions run: sudo chown -R $(stat . -c %u:%g) .trivy - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v1 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' \ No newline at end of file From 58f49b592c32f1294cf6f3d255c022d24c0a4e62 Mon Sep 17 00:00:00 2001 From: karmaking Date: Mon, 17 Feb 2025 12:46:17 +0100 Subject: [PATCH 05/23] docfix/Add Glossary Item Validate TTP Certificate --- .../glossary/TPP_Certificate_Verification.md | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 obp-api/src/main/docs/glossary/TPP_Certificate_Verification.md diff --git a/obp-api/src/main/docs/glossary/TPP_Certificate_Verification.md b/obp-api/src/main/docs/glossary/TPP_Certificate_Verification.md new file mode 100644 index 000000000..099e740fa --- /dev/null +++ b/obp-api/src/main/docs/glossary/TPP_Certificate_Verification.md @@ -0,0 +1,87 @@ +### TPP Certificate Verification + +The signing and verification flow of certificates for TPP requests is a follows: + +#### Generation of the Request by TPP + +**Step 1: Calculating the Hash for the Digest Field** + +* TPP creates the body of the request (e.g., JSON containing transaction or consent data). +* TPP applies the SHA-256 algorithm to the body of the request to generate a unique hash. +* The resulting hash is encoded in Base64 and placed in the request header under the `Digest` field. + +**Step 2: Creating the Signing String** + +* TPP prepares a signing string based on specific fields from the request header. +* The signing string is a concatenation of the values of the signed fields in a specific format. +* The order of the fields is critical and follows the specifications. + +**Step 3: Signing the Signing String** + +* TPP uses its RSA private key associated with its certificate to generate a digital signature. +* The signature is applied to the signing string using the RSA-SHA256 algorithm. +* The resulting digital signature is Base64-encoded and placed in the `Signature` field. + +**Step 4: Adding the Certificate to the TPP-Signature-Certificate Field** + +* TPP includes its public certificate in the request header under the `TPP-Signature-Certificate` field. +* The certificate is issued by a trusted certification authority (CA) and contains the TPP's public key. + +**Step 5: Sending the Request to OBP** + +* TPP sends the complete request, including the `Digest`, `Signature`, and `TPP-Signature-Certificate` headers, to the OBP endpoint. + +--- + +#### Verification of the Request by OBP + +**Step 1: Validating the TPP Certificate** + +* OBP verifies the TPP certificate included in the `TPP-Signature-Certificate` field.\ + Steps: + +1. Verify the certificate's trust chain up to a trusted certification authority. +2. Ensure the certificate is valid (not expired or revoked). +3. Confirm the certificate is issued for the appropriate usage (e.g., digital signatures for Open Banking), based on the information from the National Bank registry (e.g., certificate SN and CA found in the `Signature` field). + +* **Result**: If the certificate is valid, the TPP's identity is confirmed. + +**Step 2: Verifying the Signature in the `Signature` Field** + +* OBP extracts the fields included in the signing string based on the information in the `Signature` field. +* OBP recreates the signing string in the same format used by the TPP. +* OBP uses the public key from the certificate in the `TPP-Signature-Certificate` field to verify the signature in the `Signature` field. +* **Result**: If the signature is valid, the authenticity of the request is confirmed. + +**Step 3: Verifying the Hash in the `Digest` Field** + +* OBP computes its own SHA-256 hash of the received request body. +* The computed hash is compared with the value in the `Digest` header. +* **Result**: If the two hashes match, the integrity of the request body is confirmed. + +**Step 4: Authorizing the Request** + +* If all verifications (integrity, signature, and certificate) pass, OBP processes the request. +* Otherwise, the request is rejected with an error message (e.g., `401 Unauthorized` or `403 Forbidden`). + +--- + +#### Additional Details + +**How does OBP verify the signature in the `Signature` field?** + +1. **Extracts the public key from the `TPP-Signature-Certificate`**: + * OBP retrieves the certificate (`TPP-Signature-Certificate`) from the request header. + * From this certificate, OBP extracts the TPP's public key. +2. **Recreates the Signing String**: + * OBP analyzes the `Signature` header to identify the signed fields (e.g., `Digest`, `X-Request-ID`, `Date`, etc.). + * OBP recreates the signing string from the actual values of these fields in the specified order. +3. **Decodes the Signature**: + * OBP decodes the value in the `Signature.signature` field (the digital signature generated by TPP). + * Decoding is done using the public key extracted from the `TPP-Signature-Certificate` and the algorithm specified in `Signature.algorithm` (e.g., RSA-SHA256). +4. **Compares the Resulting Hash**: + * During decoding, OBP obtains a hash calculated by TPP at the time of signing. + * OBP compares this hash with the hash generated internally from the recreated signing string. +5. **Possible Outcomes**: + * If the hashes match, the signature is valid. + * If the hashes do not match, the signature is invalid, and the request is rejected. \ No newline at end of file From 2a46ad8d0014cb230ffa37898276065f69cfa401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 17 Feb 2025 15:15:07 +0100 Subject: [PATCH 06/23] feature/Verify the certificate's trust chain up to a trusted certification authority --- .../code/api/util/CertificateVerifier.scala | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 obp-api/src/main/scala/code/api/util/CertificateVerifier.scala diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala new file mode 100644 index 000000000..59f9bab14 --- /dev/null +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -0,0 +1,102 @@ +package code.api.util + +import java.io.ByteArrayInputStream +import java.security.KeyStore +import java.security.cert._ +import java.util.{Base64, Collections} +import javax.net.ssl.TrustManagerFactory +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +object CertificateVerifier { + + def verifyCertificate(pemCertificate: String): Boolean = { + Try { + // Convert PEM string to X.509 Certificate + val certificate = parsePemToX509Certificate(pemCertificate) + + // Load the default trust store (can be replaced with a custom one) + val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) + + + val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) + .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") + + val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) + trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit + trustStoreInputStream.close() + + + val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + // trustStore.load(null, null) // Load default trust store + trustManagerFactory.init(trustStore) + + // Get trusted CAs from the trust store + val trustAnchors = trustStore.aliases().asScala + .filter(trustStore.isCertificateEntry) + .map(alias => trustStore.getCertificate(alias).asInstanceOf[X509Certificate]) + .map(cert => new TrustAnchor(cert, null)) + .toSet + .asJava // Convert Scala Set to Java Set + + if (trustAnchors.isEmpty) throw new Exception("No trusted certificates found in trust store.") + + // Set up PKIX parameters for validation + val pkixParams = new PKIXParameters(trustAnchors) + pkixParams.setRevocationEnabled(false) // Disable CRL checks + + // Validate certificate chain + val certPath = CertificateFactory.getInstance("X.509").generateCertPath(Collections.singletonList(certificate)) + val validator = CertPathValidator.getInstance("PKIX") + validator.validate(certPath, pkixParams) + + true + } match { + case Success(_) => + println("Certificate is valid and trusted.") + true + case Failure(e: CertPathValidatorException) => + println(s"Certificate validation failed: ${e.getMessage}") + false + case Failure(e) => + println(s"Error: ${e.getMessage}") + false + } + } + + private def parsePemToX509Certificate(pem: String): X509Certificate = { + val cleanedPem = pem + .replaceAll("-----BEGIN CERTIFICATE-----", "") + .replaceAll("-----END CERTIFICATE-----", "") + .replaceAll("\\s", "") + + val decoded = Base64.getDecoder.decode(cleanedPem) + val certFactory = CertificateFactory.getInstance("X.509") + certFactory.generateCertificate(new ByteArrayInputStream(decoded)).asInstanceOf[X509Certificate] + } + + def main(args: Array[String]): Unit = { + val pemCertificate = + """-----BEGIN CERTIFICATE----- + MIIDFzCCAf+gAwIBAgIUPvfFnlyEm/bRwvPzhpfSxuI6XjkwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTAyMTQwODM3NDhaFw0yNjAyMTQwODM3NDhaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk9Mj4YgJywaCduTLjAEd3o1OqzFaj2MuI/bcdKIwPlld0n8WHp+CMkbpCD8TSAlDrjLjxcL6Homw8SM3VYUJVP/5phRNgNx7E+KzquskPUsWvTUnylLF52jLjbKVXqs6DuukGAaJNudcuJCPuGd5xDTiymRdqFL1LFxSlaqt/qRS8DV9d3/Z0JwXuHebq17pjUGluq8nkJ0N1zF5hKLdQmo9PxVULY5Kubjf2cXoH09AgJUj3RSgeScRbFxgYOhU/5OaEfQuAST0Qa8lFI6SyWQp5G08wNZGITLh/66ZissNPYIUgqGccDFKWhUNDubFF+Qyl3Gy12g8Uou6FN1qrAgMBAAGjUzBRMB0GA1UdDgQWBBSN2MfohCTpCamhcyidj2w6z6tGXDAfBgNVHSMEGDAWgBSN2MfohCTpCamhcyidj2w6z6tGXDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBYXj3L5UN8PxJAMtLT9bU4FkxyXIQM+bzvAln1ZcAfHAGb2q49oJAXy4I22f9keuq3PV7OftsjZ888rjz9QU8vMSBejWT5GV4Ln5QmQXCHonmhq6DbP7BYb4DTOXfhvk+fdg0EDdqCpzDSCXdutOjjGU6P7L0769Zjpkrnk7uuqxZ8u/FslALeuq7cerBpsOUT5CJumpQxWcUCEbFxyZJTu5SXetgKJ9Dm62AfX5H69//z88W5TUzp66Mh4AWhEa/UByJGEw9SEsjFtYhkXluz5oFee5TGWTVZRlK08UrgH9JbiuyvPc9ZNL6Ek9fV54iajqsixZCfcICICtu8hZjZ + -----END CERTIFICATE-----""" + + val isValid = verifyCertificate(pemCertificate) + println(s"Certificate verification result: $isValid") + + + val defaultTrustStore = System.getProperty("javax.net.ssl.trustStore", "Default (cacerts)") + println(s"Default Trust Store: $defaultTrustStore") + + // Load and print all certificates in the default trust store + val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) + val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) + .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") + + val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) + trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit + trustStoreInputStream.close() + + println(s"Trust Store contains ${trustStore.size()} certificates") + } +} From 6c3e68f6f71da71992bff92e0a1352e38af14fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 18 Feb 2025 17:45:50 +0100 Subject: [PATCH 07/23] feature/Verify the certificate's trust chain up to a trusted certification authority 2 --- .../resources/props/sample.props.template | 6 ++ .../code/api/util/CertificateVerifier.scala | 71 +++++++++---------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index e2a7ddba7..385f196ea 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -178,6 +178,12 @@ jwt.use.ssl=false # truststore.password.redis = truststore-password +## Trust stores is a list of trusted CA certificates +## Public certificate for the CA (used by clients and servers to validate signatures) +# truststore.path.tpp_signature = path/to/ca.p12 +# truststore.password.tpp_signature = truststore-password + + ## Enable writing API metrics (which APIs are called) to RDBMS write_metrics=true ## Enable writing connector metrics (which methods are called)to RDBMS diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 59f9bab14..4b3d2f3cf 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -1,6 +1,6 @@ package code.api.util -import java.io.ByteArrayInputStream +import java.io.{ByteArrayInputStream, FileInputStream} import java.security.KeyStore import java.security.cert._ import java.util.{Base64, Collections} @@ -10,25 +10,36 @@ import scala.util.{Failure, Success, Try} object CertificateVerifier { + // Load trust store from configured path and password + private def loadTrustStore(): Option[KeyStore] = { + val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature", "") + val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray + + Try { + val trustStore = KeyStore.getInstance("PKCS12") // Using `.p12` format + val trustStoreInputStream = new FileInputStream(trustStorePath) + trustStore.load(trustStoreInputStream, trustStorePassword) + trustStoreInputStream.close() + trustStore + } match { + case Success(store) => + println(s"✅ Loaded trust store from: $trustStorePath") + Some(store) + case Failure(e) => + println(s"❌ Failed to load trust store: ${e.getMessage}") + None + } + } + def verifyCertificate(pemCertificate: String): Boolean = { Try { - // Convert PEM string to X.509 Certificate val certificate = parsePemToX509Certificate(pemCertificate) - // Load the default trust store (can be replaced with a custom one) - val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - - - val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) - .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") - - val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) - trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit - trustStoreInputStream.close() - + // Load trust store + val trustStore = loadTrustStore() + .getOrElse(throw new Exception("Trust store could not be loaded.")) val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) - // trustStore.load(null, null) // Load default trust store trustManagerFactory.init(trustStore) // Get trusted CAs from the trust store @@ -50,23 +61,21 @@ object CertificateVerifier { val validator = CertPathValidator.getInstance("PKIX") validator.validate(certPath, pkixParams) + println("✅ Certificate is valid and trusted.") true } match { - case Success(_) => - println("Certificate is valid and trusted.") - true + case Success(_) => true case Failure(e: CertPathValidatorException) => - println(s"Certificate validation failed: ${e.getMessage}") + println(s"❌ Certificate validation failed: ${e.getMessage}") false case Failure(e) => - println(s"Error: ${e.getMessage}") + println(s"❌ Error: ${e.getMessage}") false } } private def parsePemToX509Certificate(pem: String): X509Certificate = { - val cleanedPem = pem - .replaceAll("-----BEGIN CERTIFICATE-----", "") + val cleanedPem = pem.replaceAll("-----BEGIN CERTIFICATE-----", "") .replaceAll("-----END CERTIFICATE-----", "") .replaceAll("\\s", "") @@ -82,21 +91,11 @@ object CertificateVerifier { -----END CERTIFICATE-----""" val isValid = verifyCertificate(pemCertificate) - println(s"Certificate verification result: $isValid") + println(s"✅ Certificate verification result: $isValid") - - val defaultTrustStore = System.getProperty("javax.net.ssl.trustStore", "Default (cacerts)") - println(s"Default Trust Store: $defaultTrustStore") - - // Load and print all certificates in the default trust store - val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val trustStorePath = Option(System.getProperty("javax.net.ssl.trustStore")) - .getOrElse("/usr/lib/jvm/java-17-openjdk-amd64/lib/security/cacerts") - - val trustStoreInputStream = new java.io.FileInputStream(trustStorePath) - trustStore.load(trustStoreInputStream, "changeit".toCharArray) // Default password: changeit - trustStoreInputStream.close() - - println(s"Trust Store contains ${trustStore.size()} certificates") + // Display loaded trust store info + loadTrustStore().foreach { trustStore => + println(s"🔹 Trust Store contains ${trustStore.size()} certificates.") + } } } From 49f7efe0588a5b13bb881448667af1dbc25164cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 18 Feb 2025 17:46:31 +0100 Subject: [PATCH 08/23] bugfix/Load the CA certificate of Redis --- obp-api/src/main/scala/code/api/cache/Redis.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 4c5412125..ed5d6856c 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -61,7 +61,7 @@ object Redis extends MdcLoggable { // Load the CA certificate val trustStore = KeyStore.getInstance(KeyStore.getDefaultType) - val trustStorePassword = APIUtil.getPropsValue("keystore.password.redis") + val trustStorePassword = APIUtil.getPropsValue("truststore.password.redis") .getOrElse(APIUtil.initPasswd).toCharArray val truststorePath = APIUtil.getPropsValue("truststore.path.redis").getOrElse("") val trustStoreStream = new FileInputStream(truststorePath) From b33d9a33669ddb5f1a1fd6770dbda69b80563da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 19 Feb 2025 12:05:15 +0100 Subject: [PATCH 09/23] feature/Verify the certificate's trust chain up to a trusted certification authority 3 --- .../code/api/util/CertificateVerifier.scala | 92 +++++++++++++++---- 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 4b3d2f3cf..4a84db170 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -1,36 +1,66 @@ package code.api.util +import code.util.Helper.MdcLoggable + import java.io.{ByteArrayInputStream, FileInputStream} import java.security.KeyStore import java.security.cert._ import java.util.{Base64, Collections} import javax.net.ssl.TrustManagerFactory +import scala.io.Source import scala.jdk.CollectionConverters._ import scala.util.{Failure, Success, Try} -object CertificateVerifier { +object CertificateVerifier extends MdcLoggable { - // Load trust store from configured path and password + /** + * Loads a trust store (`.p12` file) from a configured path. + * + * This function: + * - Reads the trust store password from the application properties (`truststore.path.tpp_signature`). + * - Uses Java's `KeyStore` class to load the certificates. + * - If successful, logs `✅ Loaded trust store from: path`. + * - If it fails, logs `❌ Failed to load trust store: error message`. + * + * @return An `Option[KeyStore]` containing the loaded trust store, or `None` if loading fails. + */ private def loadTrustStore(): Option[KeyStore] = { - val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature", "") + val trustStorePath = APIUtil.getPropsValue("truststore.path.tpp_signature") + .or(APIUtil.getPropsValue("truststore.path")).getOrElse("") val trustStorePassword = APIUtil.getPropsValue("truststore.password.tpp_signature", "").toCharArray Try { - val trustStore = KeyStore.getInstance("PKCS12") // Using `.p12` format + val trustStore = KeyStore.getInstance("PKCS12") val trustStoreInputStream = new FileInputStream(trustStorePath) - trustStore.load(trustStoreInputStream, trustStorePassword) - trustStoreInputStream.close() + try { + trustStore.load(trustStoreInputStream, trustStorePassword) + } finally { + trustStoreInputStream.close() + } trustStore } match { case Success(store) => - println(s"✅ Loaded trust store from: $trustStorePath") + logger.info(s"✅ Loaded trust store from: $trustStorePath") Some(store) case Failure(e) => - println(s"❌ Failed to load trust store: ${e.getMessage}") + logger.info(s"❌ Failed to load trust store: ${e.getMessage}") None } } + /** + * Verifies an X.509 certificate against the loaded trust store. + * + * This function: + * - Parses the PEM certificate into an `X509Certificate` using `parsePemToX509Certificate`. + * - Loads the trust store using `loadTrustStore()`. + * - Extracts trusted root CAs from the trust store. + * - Creates PKIX validation parameters and disables revocation checking. + * - Validates the certificate using Java's `CertPathValidator`. + * + * @param pemCertificate The X.509 certificate in PEM format. + * @return `true` if the certificate is valid and trusted, otherwise `false`. + */ def verifyCertificate(pemCertificate: String): Boolean = { Try { val certificate = parsePemToX509Certificate(pemCertificate) @@ -61,19 +91,30 @@ object CertificateVerifier { val validator = CertPathValidator.getInstance("PKIX") validator.validate(certPath, pkixParams) - println("✅ Certificate is valid and trusted.") + logger.info("✅ Certificate is valid and trusted.") true } match { case Success(_) => true case Failure(e: CertPathValidatorException) => - println(s"❌ Certificate validation failed: ${e.getMessage}") + logger.info(s"❌ Certificate validation failed: ${e.getMessage}") false case Failure(e) => - println(s"❌ Error: ${e.getMessage}") + logger.info(s"❌ Error: ${e.getMessage}") false } } + /** + * Converts a PEM certificate (Base64-encoded) into an `X509Certificate` object. + * + * This function: + * - Removes the PEM header and footer (`-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----`). + * - Decodes the Base64-encoded certificate data. + * - Generates and returns an `X509Certificate` object. + * + * @param pem The X.509 certificate in PEM format. + * @return The parsed `X509Certificate` object. + */ private def parsePemToX509Certificate(pem: String): X509Certificate = { val cleanedPem = pem.replaceAll("-----BEGIN CERTIFICATE-----", "") .replaceAll("-----END CERTIFICATE-----", "") @@ -84,18 +125,31 @@ object CertificateVerifier { certFactory.generateCertificate(new ByteArrayInputStream(decoded)).asInstanceOf[X509Certificate] } + def loadPemCertificateFromFile(filePath: String): Option[String] = { + Try { + val source = Source.fromFile(filePath) + try source.getLines().mkString("\n") // Read entire file into a single string + finally source.close() + } match { + case Success(pem) => Some(pem) + case Failure(exception) => + println(s"❌ Failed to load PEM certificate from file: ${exception.getMessage}") + None + } + } + def main(args: Array[String]): Unit = { - val pemCertificate = - """-----BEGIN CERTIFICATE----- - MIIDFzCCAf+gAwIBAgIUPvfFnlyEm/bRwvPzhpfSxuI6XjkwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw0yNTAyMTQwODM3NDhaFw0yNjAyMTQwODM3NDhaMBsxGTAXBgNVBAMMEFRlc3QgQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk9Mj4YgJywaCduTLjAEd3o1OqzFaj2MuI/bcdKIwPlld0n8WHp+CMkbpCD8TSAlDrjLjxcL6Homw8SM3VYUJVP/5phRNgNx7E+KzquskPUsWvTUnylLF52jLjbKVXqs6DuukGAaJNudcuJCPuGd5xDTiymRdqFL1LFxSlaqt/qRS8DV9d3/Z0JwXuHebq17pjUGluq8nkJ0N1zF5hKLdQmo9PxVULY5Kubjf2cXoH09AgJUj3RSgeScRbFxgYOhU/5OaEfQuAST0Qa8lFI6SyWQp5G08wNZGITLh/66ZissNPYIUgqGccDFKWhUNDubFF+Qyl3Gy12g8Uou6FN1qrAgMBAAGjUzBRMB0GA1UdDgQWBBSN2MfohCTpCamhcyidj2w6z6tGXDAfBgNVHSMEGDAWgBSN2MfohCTpCamhcyidj2w6z6tGXDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBYXj3L5UN8PxJAMtLT9bU4FkxyXIQM+bzvAln1ZcAfHAGb2q49oJAXy4I22f9keuq3PV7OftsjZ888rjz9QU8vMSBejWT5GV4Ln5QmQXCHonmhq6DbP7BYb4DTOXfhvk+fdg0EDdqCpzDSCXdutOjjGU6P7L0769Zjpkrnk7uuqxZ8u/FslALeuq7cerBpsOUT5CJumpQxWcUCEbFxyZJTu5SXetgKJ9Dm62AfX5H69//z88W5TUzp66Mh4AWhEa/UByJGEw9SEsjFtYhkXluz5oFee5TGWTVZRlK08UrgH9JbiuyvPc9ZNL6Ek9fV54iajqsixZCfcICICtu8hZjZ - -----END CERTIFICATE-----""" + // val certificatePath = "/path/to/certificate.pem" + val certificatePath = "/home/marko/Downloads/BerlinGroupSigning/certificate.pem" + val pemCertificate = loadPemCertificateFromFile(certificatePath) - val isValid = verifyCertificate(pemCertificate) - println(s"✅ Certificate verification result: $isValid") + pemCertificate.foreach { pem => + val isValid = verifyCertificate(pem) + logger.info(s"✅ Certificate verification result: $isValid") + } - // Display loaded trust store info loadTrustStore().foreach { trustStore => - println(s"🔹 Trust Store contains ${trustStore.size()} certificates.") + logger.info(s"🔹 Trust Store contains ${trustStore.size()} certificates.") } } } From fc91b2ec255d809c3afda78bd2fac5fc80f6d40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Thu, 20 Feb 2025 10:47:31 +0100 Subject: [PATCH 10/23] feature/Verify the certificate's trust chain up to a trusted certification authority 4 --- .../src/main/scala/code/api/util/BerlinGroupSigning.scala | 7 ++++++- .../src/main/scala/code/api/util/CertificateVerifier.scala | 4 ++-- obp-api/src/main/scala/code/api/util/ErrorMessages.scala | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala index 3cb253db0..c63fc89cc 100644 --- a/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala +++ b/obp-api/src/main/scala/code/api/util/BerlinGroupSigning.scala @@ -130,7 +130,12 @@ object BerlinGroupSigning { val signatureHeaderValue = getHeaderValue(RequestHeader.Signature, requestHeaders) val signature = parseSignatureHeader(signatureHeaderValue).getOrElse("signature", "NONE") val isVerified = verifySignature(signingString, signature, certificatePem) - if (isVerified) forwardResult else (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) + val isValidated = CertificateVerifier.validateCertificate(certificatePem) + (isVerified, isValidated) match { + case (true, true) => forwardResult + case (true, false) => (Failure(ErrorMessages.X509PublicKeyCannotBeValidated), forwardResult._2) + case (false, _) => (Failure(ErrorMessages.X509PublicKeyCannotVerify), forwardResult._2) + } case Failure(msg, t, c) => (Failure(msg, t, c), forwardResult._2) // PEM certificate is not valid case _ => (Failure(ErrorMessages.X509GeneralError), forwardResult._2) // PEM certificate cannot be validated } diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 4a84db170..848c4efc5 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -61,7 +61,7 @@ object CertificateVerifier extends MdcLoggable { * @param pemCertificate The X.509 certificate in PEM format. * @return `true` if the certificate is valid and trusted, otherwise `false`. */ - def verifyCertificate(pemCertificate: String): Boolean = { + def validateCertificate(pemCertificate: String): Boolean = { Try { val certificate = parsePemToX509Certificate(pemCertificate) @@ -144,7 +144,7 @@ object CertificateVerifier extends MdcLoggable { val pemCertificate = loadPemCertificateFromFile(certificatePath) pemCertificate.foreach { pem => - val isValid = verifyCertificate(pem) + val isValid = validateCertificate(pem) logger.info(s"✅ Certificate verification result: $isValid") } diff --git a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala index 37061e2c2..e26c48520 100644 --- a/obp-api/src/main/scala/code/api/util/ErrorMessages.scala +++ b/obp-api/src/main/scala/code/api/util/ErrorMessages.scala @@ -279,6 +279,7 @@ object ErrorMessages { val X509ThereAreNoPsd2Roles = "OBP-20308: PEM Encoded Certificate does not contain PSD2 roles." val X509CannotGetPublicKey = "OBP-20309: Public key cannot be found in the PEM Encoded Certificate." val X509PublicKeyCannotVerify = "OBP-20310: Certificate's public key cannot be used to verify signed request." + val X509PublicKeyCannotBeValidated = "OBP-20312: Certificate's public key cannot be validated." val X509RequestIsNotSigned = "OBP-20311: The Request is not signed." // OpenID Connect From dd1003a4c123d1f319893f287b7a812ff47f41bc Mon Sep 17 00:00:00 2001 From: Hongwei Date: Thu, 20 Feb 2025 14:04:42 +0100 Subject: [PATCH 11/23] bugfix/tweaked the password regex- added [ and ] --- .../main/scala/code/api/util/APIUtil.scala | 37 ++++++++---- .../test/scala/code/util/APIUtilTest.scala | 60 +++++++++++++++++++ 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/APIUtil.scala b/obp-api/src/main/scala/code/api/util/APIUtil.scala index 121cc6bd7..745417dd9 100644 --- a/obp-api/src/main/scala/code/api/util/APIUtil.scala +++ b/obp-api/src/main/scala/code/api/util/APIUtil.scala @@ -784,16 +784,24 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ * (?=.*\d) //should contain at least one digit * (?=.*[a-z]) //should contain at least one lower case * (?=.*[A-Z]) //should contain at least one upper case - * (?=.*[!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]) //should contain at least one special character - * ([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]{10,16}) //should contain 10 to 16 valid characters + * (?=.*[!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]) //should contain at least one special character + * ([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]{10,16}) //should contain 10 to 16 valid characters **/ val regex = - """^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~])([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~]{10,16})$""".r - password match { - case password if(password.length > 16 && password.length <= 512 && basicPasswordValidation(password) ==SILENCE_IS_GOLDEN) => true - case regex(password) if(basicPasswordValidation(password) ==SILENCE_IS_GOLDEN) => true - case _ => false + """^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]])([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~\[\]]{10,16})$""".r + + // first check `basicPasswordValidation` + if (basicPasswordValidation(password) != SILENCE_IS_GOLDEN) { + return false } + + // 2nd: check the password length between 10 and 512 + if (password.length > 16 && password.length <= 512) { + return true + } + + // 3rd: check the regular expression + regex.pattern.matcher(password).matches() } /** only A-Z, a-z, 0-9,-,_,. =, & and max length <= 2048 */ @@ -847,12 +855,17 @@ object APIUtil extends MdcLoggable with CustomJsonFormats{ /** also support space now */ def basicPasswordValidation(value:String): String ={ val valueLength = value.length - val regex = """^([A-Za-z0-9!"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~ ]+)$""".r - value match { - case regex(e) if(valueLength <= 512) => SILENCE_IS_GOLDEN - case regex(e) if(valueLength > 512) => ErrorMessages.InvalidValueLength - case _ => ErrorMessages.InvalidValueCharacters + val regex = """^([A-Za-z0-9!\"#$%&'\(\)*+,-./:;<=>?@\\[\\\\]^_\\`{|}~ \[\]]+)$""".r + + if (!regex.pattern.matcher(value).matches()) { + return ErrorMessages.InvalidValueCharacters } + + if (valueLength > 512) { + return ErrorMessages.InvalidValueLength + } + + SILENCE_IS_GOLDEN } /** only A-Z, a-z, 0-9, -, _, ., @, and max length <= 512 */ diff --git a/obp-api/src/test/scala/code/util/APIUtilTest.scala b/obp-api/src/test/scala/code/util/APIUtilTest.scala index 450def8dc..3ee21c4cb 100644 --- a/obp-api/src/test/scala/code/util/APIUtilTest.scala +++ b/obp-api/src/test/scala/code/util/APIUtilTest.scala @@ -37,6 +37,7 @@ import code.api.util._ import code.setup.PropsReset import code.util.Helper.SILENCE_IS_GOLDEN import com.openbankproject.commons.model.UserAuthContextCommons +import com.github.dwickern.macros.NameOf.nameOf import net.liftweb.common.{Box, Empty, Full} import net.liftweb.http.provider.HTTPParam import net.liftweb.json.{JValue, parse} @@ -838,6 +839,65 @@ class APIUtilTest extends FeatureSpec with Matchers with GivenWhenThen with Prop actualValue6 contains (InvalidValueLength) shouldBe (true) } + + scenario(s"Test the ${nameOf(APIUtil.basicPasswordValidation _)} method") { + val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]" + + basicPasswordValidation(firefoxStrongPasswordProposal) shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation("Abc!123 xyz") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation("SuperStrong#123") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation("Hello World!") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation(" ") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN allow space so far + + basicPasswordValidation("short💥") shouldBe (InvalidValueCharacters) // ❌ ErrorMessages.InvalidValueCharacters + basicPasswordValidation("a" * 513) shouldBe (InvalidValueLength) // ❌ ErrorMessages.InvalidValueLength + + } + + scenario(s"Test the ${nameOf(APIUtil.fullPasswordValidation _)} method") { + val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]" + + fullPasswordValidation(firefoxStrongPasswordProposal) // ✅ true + fullPasswordValidation("Abc!123xyz") // ✅ true + fullPasswordValidation("SuperStrong#123") // ✅ true + fullPasswordValidation("Abcdefg!1") // ✅ true + fullPasswordValidation("short1!") // ❌ false(too short) + fullPasswordValidation("alllowercase123!") // ❌ false(no capital letter) + fullPasswordValidation("ALLUPPERCASE123!") // ❌ false(no smaller case letter) + fullPasswordValidation("NoSpecialChar123") // ❌ false(not special character) + + } + + } + + feature(s"test ${nameOf(APIUtil.basicPasswordValidation _)} and ${nameOf(APIUtil.fullPasswordValidation _)}") { + + scenario(s"Test the ${nameOf(APIUtil.basicPasswordValidation _)} method") { + val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]" + + basicPasswordValidation(firefoxStrongPasswordProposal) shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation("Abc!123 xyz") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation("SuperStrong#123") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation("Hello World!") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN + basicPasswordValidation(" ") shouldBe (SILENCE_IS_GOLDEN) // ✅ SILENCE_IS_GOLDEN allow space so far + + basicPasswordValidation("short💥") shouldBe (InvalidValueCharacters) // ❌ ErrorMessages.InvalidValueCharacters + basicPasswordValidation("a" * 513) shouldBe (InvalidValueLength) // ❌ ErrorMessages.InvalidValueLength + + } + + scenario(s"Test the ${nameOf(APIUtil.fullPasswordValidation _)} method") { + val firefoxStrongPasswordProposal = "9YF]gZnXzAENM+]" + + fullPasswordValidation(firefoxStrongPasswordProposal) // ✅ true + fullPasswordValidation("Abc!123xyz") // ✅ true + fullPasswordValidation("SuperStrong#123") // ✅ true + fullPasswordValidation("Abcdefg!1") // ✅ true + fullPasswordValidation("short1!") // ❌ false(too short) + fullPasswordValidation("alllowercase123!") // ❌ false(no capital letter) + fullPasswordValidation("ALLUPPERCASE123!") // ❌ false(no smaller case letter) + fullPasswordValidation("NoSpecialChar123") // ❌ false(not special character) + } } From ade36321ff37c4b801e73d848247c49ccd8c483b Mon Sep 17 00:00:00 2001 From: Hongwei Date: Thu, 20 Feb 2025 16:13:34 +0100 Subject: [PATCH 12/23] refactor/tweaked the variable names --- .../ResourceDocsAPIMethods.scala | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala index fc2935f7d..ff0d92a9a 100644 --- a/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala +++ b/obp-api/src/main/scala/code/api/ResourceDocs1_4_0/ResourceDocsAPIMethods.scala @@ -519,7 +519,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth case Some(STATIC) => { val cacheValueFromRedis = Caching.getStaticResourceDocCache(cacheKey) - val dynamicDocs: Box[JValue] = + val staticDocs: Box[JValue] = if (cacheValueFromRedis.isDefined) { Full(json.parse(cacheValueFromRedis.get)) } else { @@ -530,12 +530,12 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth Full(resourceDocJsonJValue) } - Future(dynamicDocs.map(successJsonResponse(_))) + Future(staticDocs.map(successJsonResponse(_))) } case _ => { val cacheValueFromRedis = Caching.getAllResourceDocCache(cacheKey) - val dynamicDocs: Box[JValue] = + val bothStaticAndDyamicDocs: Box[JValue] = if (cacheValueFromRedis.isDefined) { Full(json.parse(cacheValueFromRedis.get)) } else { @@ -546,7 +546,7 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth Full(resourceDocJsonJValue) } - Future(dynamicDocs.map(successJsonResponse(_))) + Future(bothStaticAndDyamicDocs.map(successJsonResponse(_))) } } } @@ -715,13 +715,11 @@ trait ResourceDocsAPIMethods extends MdcLoggable with APIMethods220 with APIMeth swaggerJValue <- NewStyle.function.tryons(s"$UnknownError Can not convert internal swagger file.", 400, cc.callContext) { val cacheValueFromRedis = Caching.getStaticSwaggerDocCache(cacheKey) - val dynamicDocs: JValue = - if (cacheValueFromRedis.isDefined) { - json.parse(cacheValueFromRedis.get) - } else { - convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered) - } - dynamicDocs + if (cacheValueFromRedis.isDefined) { + json.parse(cacheValueFromRedis.get) + } else { + convertResourceDocsToSwaggerJvalueAndSetCache(cacheKey, requestedApiVersionString, resourceDocsJsonFiltered) + } } } yield { (swaggerJValue, HttpCode.`200`(cc.callContext)) From 4ddc3e627b6231fb9fc4b269bcaac4c1c4e454e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Fri, 21 Feb 2025 17:09:14 +0100 Subject: [PATCH 13/23] feature/Implement BG consent flow when user chooses accounts to link at confirmation step --- .../code/snippet/BerlinGroupConsent.scala | 225 ++++++++++++++++-- .../webapp/confirm-bg-consent-request.html | 2 +- 2 files changed, 203 insertions(+), 24 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 9b8956ef9..8d396dae9 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -26,30 +26,86 @@ TESOBE (http://www.tesobe.com/) */ package code.snippet +import code.accountholders.AccountHolders import code.api.RequestHeader -import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{GetConsentResponseJson, createGetConsentResponseJson} -import code.api.util.{ConsentJWT, CustomJsonFormats, JwtUtil} +import code.api.berlin.group.v1_3.JSONFactory_BERLIN_GROUP_1_3.{ConsentAccessAccountsJson, ConsentAccessJson, GetConsentResponseJson, createGetConsentResponseJson} +import code.api.util.ErrorMessages.ConsentNotFound +import code.api.util._ import code.api.v3_1_0.APIMethods310 import code.api.v5_0_0.APIMethods500 import code.api.v5_1_0.APIMethods510 import code.consent.{ConsentStatus, Consents, MappedConsent} import code.consumer.Consumers -import code.model.dataAccess.AuthUser +import code.model.dataAccess.{AuthUser, BankAccountRouting} import code.util.Helper.{MdcLoggable, ObpS} +import com.openbankproject.commons.ExecutionContext.Implicits.global +import com.openbankproject.commons.model.BankIdAccountId import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.http.js.JsCmds import net.liftweb.http.rest.RestHelper -import net.liftweb.http.{RequestVar, S, SHtml, SessionVar} +import net.liftweb.http.{S, SHtml, SessionVar} import net.liftweb.json.{Formats, parse} +import net.liftweb.mapper.By import net.liftweb.util.CssSel import net.liftweb.util.Helpers._ import scala.collection.immutable +import scala.concurrent.Future +import scala.xml.NodeSeq class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 { protected implicit override def formats: Formats = CustomJsonFormats.formats private object otpValue extends SessionVar("123") private object redirectUriValue extends SessionVar("") + private object updateConsentPayloadValue extends SessionVar(false) + private object userIsOwnerOfAccountsValue extends SessionVar(true) + + // Separate session variables for accounts, balances, and transactions + private object selectedAccountsIbansValue extends SessionVar[Set[String]](Set()) { + override def set(value: Set[String]): Set[String] = { + logger.debug(s"selectedAccountsIbansValue changed to: ${value.mkString(", ")}") + super.set(value) + } + } + + private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) + + private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) + + // Function to transform a list of IBANs into ConsentAccessJson + def createConsentAccessJson(accounts: List[String], balances: List[String], transactions: List[String]): ConsentAccessJson = { + val accountsList = accounts.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + val balancesList = balances.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + val transactionsList = transactions.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) + + ConsentAccessJson( + accounts = Some(accountsList), // Populate accounts + balances = Some(balancesList), // Populate balances + transactions = Some(transactionsList) // Populate transactions + ) + } + + private def updateConsent(consentId: String, ibansAccount: List[String], ibansBalance: List[String], ibansTransaction: List[String]): Future[MappedConsent] = { + for { + consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { + APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404) + } + consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( + createConsentAccessJson(ibansAccount, ibansBalance, ibansTransaction), + consent, + None + ) map { + i => APIUtil.connectorEmptyResponse(i, None) + } + updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map { + i => APIUtil.connectorEmptyResponse(i, None) + } + } yield { + updatedConsent + } + } + def confirmBerlinGroupConsentRequest: CssSel = { callGetConsentByConsentId() match { @@ -65,25 +121,137 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 val consumerRedirectUri: Option[String] = consumer.map(_.redirectURL.get).toOption val uri: String = tppRedirectUri.headOption.orElse(consumerRedirectUri).getOrElse("https://not.defined.com") redirectUriValue.set(uri) + + //Get All OBP accounts from `Account Holder` table, source == null --> mean accounts are created by OBP endpoints, not from User Auth Context, + // Step 1: Get all accounts held by the current user + val userAccounts: Set[BankIdAccountId] = + AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet + val userIbans: Set[String] = userAccounts.flatMap { acc => + BankAccountRouting.find( + By(BankAccountRouting.BankId, acc.bankId.value), + By(BankAccountRouting.AccountId, acc.accountId.value), + By(BankAccountRouting.AccountRoutingScheme, "IBAN") + ).map(_.AccountRoutingAddress.get) + } + + + val canReadAccountsIbans: List[String] = json.access.accounts match { + case Some(accounts) if accounts.isEmpty => + updateConsentPayloadValue.set(true) + userIbans.toList + case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => + accounts.flatMap(_.iban) + case Some(accounts) => + userIsOwnerOfAccountsValue.set(false) + accounts.flatMap(_.iban) + case None => List() + } + val canReadBalancesIbans: List[String] = json.access.balances match { + case Some(balances) if balances.isEmpty => + updateConsentPayloadValue.set(true) + userIbans.toList + case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => + balances.flatMap(_.iban) + case Some(balances) => + userIsOwnerOfAccountsValue.set(false) + balances.flatMap(_.iban) + case None => List() + } + val canReadTransactionsIbans: List[String] = json.access.transactions match { + case Some(transactions) if transactions.isEmpty => + updateConsentPayloadValue.set(true) + userIbans.toList + case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => + transactions.flatMap(_.iban) + case Some(transactions) => + userIsOwnerOfAccountsValue.set(false) + transactions.flatMap(_.iban) + case None => List() + } + + /// Function to generate toggle switches for IBAN lists + def generateCheckboxes(scope: String, ibans: List[String], selectedList: Set[String], sessionVar: SessionVar[Set[String]]): immutable.Seq[NodeSeq] = { + ibans.map { iban => + if (updateConsentPayloadValue.is) { + // Show toggle switch when updateConsentPayloadValue is true +
+ + + {iban} + +
+ } else { + // Show only the IBAN text when updateConsentPayloadValue is false + + {iban} + + } + } + } + + + // Form text and user details + val currentUser = AuthUser.currentUser + val firstName = currentUser.map(_.firstName.get).getOrElse("") + val lastName = currentUser.map(_.lastName.get).getOrElse("") + val consumerName = consumer.map(_.name.get).getOrElse("") val formText = - s"""I, ${AuthUser.currentUser.map(_.firstName.get).getOrElse("")} ${AuthUser.currentUser.map(_.lastName.get).getOrElse("")}, consent to the service provider ${consumer.map(_.name.get).getOrElse("")} making actions on my behalf. - | - |This consent must respects the following actions: - | - | 1) Can read accounts: ${json.access.accounts.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | 2) Can read balances: ${json.access.balances.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | 3) Can read transactions: ${json.access.transactions.getOrElse(Nil).flatMap(_.iban).mkString(", ")} - | - |This consent will end on date ${json.validUntil}. - | - |I understand that I can revoke this consent at any time. - |""".stripMargin + s"""I, $firstName $lastName, consent to the service provider $consumerName making the following actions on my behalf: + |""".stripMargin + // Converting formText into a NodeSeq for raw HTML + val formTextHtml: NodeSeq = scala.xml.XML.loadString("
" + formText + "
") - "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & - "#confirm-bg-consent-request-form-text *" #> s"""$formText""" & + // Form rendering + "#confirm-bg-consent-request-form-title *" #> "Please confirm or deny the following consent request:" & + "#confirm-bg-consent-request-form-text *" #> ( +
+

+ {formTextHtml} +

+ +

1) Read account (basic) details of:

+
+ {generateCheckboxes("canReadAccountsIbans", canReadAccountsIbans, selectedAccountsIbansValue.is, selectedAccountsIbansValue)} +
+
+ +

2) Read account balances of:

+
+ {generateCheckboxes("canReadBalancesIbans", canReadBalancesIbans, selectedBalancesIbansValue.is, selectedBalancesIbansValue)} +
+
+ +

3) Read transactions of:

+
+ {generateCheckboxes("canReadTransactionsIbans", canReadTransactionsIbans, selectedTransactionsIbansValue.is, selectedTransactionsIbansValue)} +
+
+ +

This consent will end on date + {json.validUntil} + .

+

I understand that I can revoke this consent at any time.

+
+ ) & { + if (userIsOwnerOfAccountsValue) { "#confirm-bg-consent-request-confirm-submit-button" #> SHtml.onSubmitUnit(confirmConsentRequestProcess) & - "#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess) + "#confirm-bg-consent-request-deny-submit-button" #> SHtml.onSubmitUnit(denyConsentRequestProcess) + } else { + S.error(s"User $firstName $lastName is not owner of listed accounts") + "#confirm-bg-consent-request-confirm-submit-button" #> "" & + "#confirm-bg-consent-request-deny-submit-button" #> "" + }} + case everythingElse => S.error(everythingElse.toString) "#confirm-bg-consent-request-form-title *" #> s"Please confirm or deny the following consent request:" & @@ -104,10 +272,21 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } private def confirmConsentRequestProcess() = { - val consentId = ObpS.param("CONSENT_ID") openOr ("") - S.redirectTo( - s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" - ) + if(selectedAccountsIbansValue.is.isEmpty && + selectedBalancesIbansValue.is.isEmpty && + selectedTransactionsIbansValue.is.isEmpty) + { + S.error(s"Please select at least 1 account") + } else { + val consentId = ObpS.param("CONSENT_ID") openOr ("") + if (updateConsentPayloadValue.is) { + updateConsent(consentId, selectedAccountsIbansValue.is.toList, selectedBalancesIbansValue.is.toList, selectedTransactionsIbansValue.is.toList) + } + S.redirectTo( + s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" + ) + } + } private def denyConsentRequestProcess() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") diff --git a/obp-api/src/main/webapp/confirm-bg-consent-request.html b/obp-api/src/main/webapp/confirm-bg-consent-request.html index d3e2ffcff..d07e0318a 100644 --- a/obp-api/src/main/webapp/confirm-bg-consent-request.html +++ b/obp-api/src/main/webapp/confirm-bg-consent-request.html @@ -32,7 +32,7 @@ Berlin 13359, Germany
From 9a2f4b1fb7d7666c9e9eecd98e001c50a38bed12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Mon, 24 Feb 2025 13:51:49 +0100 Subject: [PATCH 14/23] docfix/Implement BG consent flow when user chooses accounts to link at confirmation step --- .../code/snippet/BerlinGroupConsent.scala | 147 ++++++++++++------ 1 file changed, 100 insertions(+), 47 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index 8d396dae9..f082b4455 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -1,29 +1,28 @@ /** -Open Bank Project - API -Copyright (C) 2011-2019, TESOBE GmbH. - -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 . - -Email: contact@tesobe.com -TESOBE GmbH. -Osloer Strasse 16/17 -Berlin 13359, Germany - -This product includes software developed at -TESOBE (http://www.tesobe.com/) - - */ + * Open Bank Project - API + * Copyright (C) 2011-2019, TESOBE GmbH. + * + * 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 . + * + * Email: contact@tesobe.com + * TESOBE GmbH. + * Osloer Strasse 16/17 + * Berlin 13359, Germany + * + * This product includes software developed at + * TESOBE (http://www.tesobe.com/) + */ package code.snippet import code.accountholders.AccountHolders @@ -53,15 +52,22 @@ import scala.collection.immutable import scala.concurrent.Future import scala.xml.NodeSeq +/** + * This class handles Berlin Group consent requests. + * It provides functionality to confirm or deny consent requests, + * and manages the consent process for accessing account data. + */ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 with APIMethods500 with APIMethods310 { + // Custom JSON formats for serialization/deserialization protected implicit override def formats: Formats = CustomJsonFormats.formats - private object otpValue extends SessionVar("123") - private object redirectUriValue extends SessionVar("") - private object updateConsentPayloadValue extends SessionVar(false) - private object userIsOwnerOfAccountsValue extends SessionVar(true) + // Session variables to store OTP, redirect URI, and other consent-related data + private object otpValue extends SessionVar("123") // Stores the OTP value for SCA (Strong Customer Authentication) + private object redirectUriValue extends SessionVar("") // Stores the redirect URI for post-consent actions + private object updateConsentPayloadValue extends SessionVar(false) // Flag to indicate if consent payload needs updating + private object userIsOwnerOfAccountsValue extends SessionVar(true) // Flag to check if the user owns the accounts - // Separate session variables for accounts, balances, and transactions + // Session variables to store selected IBANs for accounts, balances, and transactions private object selectedAccountsIbansValue extends SessionVar[Set[String]](Set()) { override def set(value: Set[String]): Set[String] = { logger.debug(s"selectedAccountsIbansValue changed to: ${value.mkString(", ")}") @@ -69,11 +75,17 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) + private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for balances + private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for transactions - private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) - - // Function to transform a list of IBANs into ConsentAccessJson + /** + * Creates a ConsentAccessJson object from lists of IBANs for accounts, balances, and transactions. + * + * @param accounts List of IBANs for accounts. + * @param balances List of IBANs for balances. + * @param transactions List of IBANs for transactions. + * @return ConsentAccessJson object. + */ def createConsentAccessJson(accounts: List[String], balances: List[String], transactions: List[String]): ConsentAccessJson = { val accountsList = accounts.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) val balancesList = balances.map(iban => ConsentAccessAccountsJson(iban = Some(iban), None, None, None, None, None)) @@ -86,11 +98,22 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ) } + /** + * Updates the consent with new IBANs for accounts, balances, and transactions. + * + * @param consentId The ID of the consent to update. + * @param ibansAccount List of IBANs for accounts. + * @param ibansBalance List of IBANs for balances. + * @param ibansTransaction List of IBANs for transactions. + * @return Future[MappedConsent] representing the updated consent. + */ private def updateConsent(consentId: String, ibansAccount: List[String], ibansBalance: List[String], ibansTransaction: List[String]): Future[MappedConsent] = { for { + // Fetch the consent by ID consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { APIUtil.unboxFullOrFail(_, None, s"$ConsentNotFound ($consentId)", 404) } + // Update the consent JWT with new access details consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( createConsentAccessJson(ibansAccount, ibansBalance, ibansTransaction), consent, @@ -98,6 +121,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ) map { i => APIUtil.connectorEmptyResponse(i, None) } + // Save the updated consent updatedConsent <- Future(Consents.consentProvider.vend.setJsonWebToken(consent.consentId, consentJWT)) map { i => APIUtil.connectorEmptyResponse(i, None) } @@ -106,26 +130,30 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - + /** + * Renders the Berlin Group consent confirmation form. + * + * @return CssSel for rendering the form. + */ def confirmBerlinGroupConsentRequest: CssSel = { callGetConsentByConsentId() match { case Full(consent) => + // Set OTP and redirect URI from the consent otpValue.set(consent.challenge) val json: GetConsentResponseJson = createGetConsentResponseJson(consent) val consumer = Consumers.consumers.vend.getConsumerByConsumerId(consent.consumerId) val consentJwt: Box[ConsentJWT] = JwtUtil.getSignedPayloadAsJson(consent.jsonWebToken).map(parse(_) .extract[ConsentJWT]) - val tppRedirectUri: immutable.Seq[String] = consentJwt.map{ h => + val tppRedirectUri: immutable.Seq[String] = consentJwt.map { h => h.request_headers.filter(h => h.name == RequestHeader.`TPP-Redirect-URL`) }.getOrElse(Nil).map((_.values.mkString(""))) val consumerRedirectUri: Option[String] = consumer.map(_.redirectURL.get).toOption val uri: String = tppRedirectUri.headOption.orElse(consumerRedirectUri).getOrElse("https://not.defined.com") redirectUriValue.set(uri) - //Get All OBP accounts from `Account Holder` table, source == null --> mean accounts are created by OBP endpoints, not from User Auth Context, - // Step 1: Get all accounts held by the current user + // Get all accounts held by the current user val userAccounts: Set[BankIdAccountId] = - AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet + AccountHolders.accountHolders.vend.getAccountsHeldByUser(AuthUser.currentUser.flatMap(_.user.foreign).openOrThrowException(ErrorMessages.UserNotLoggedIn), Some(null)).toSet val userIbans: Set[String] = userAccounts.flatMap { acc => BankAccountRouting.find( By(BankAccountRouting.BankId, acc.bankId.value), @@ -134,7 +162,7 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ).map(_.AccountRoutingAddress.get) } - + // Determine which IBANs the user can access for accounts, balances, and transactions val canReadAccountsIbans: List[String] = json.access.accounts match { case Some(accounts) if accounts.isEmpty => updateConsentPayloadValue.set(true) @@ -169,7 +197,15 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 case None => List() } - /// Function to generate toggle switches for IBAN lists + /** + * Generates toggle switches for IBAN lists. + * + * @param scope The scope of the IBANs (e.g., "canReadAccountsIbans"). + * @param ibans List of IBANs to display. + * @param selectedList Set of currently selected IBANs. + * @param sessionVar Session variable to update when toggling. + * @return Sequence of NodeSeq representing the toggle switches. + */ def generateCheckboxes(scope: String, ibans: List[String], selectedList: Set[String], sessionVar: SessionVar[Set[String]]): immutable.Seq[NodeSeq] = { ibans.map { iban => if (updateConsentPayloadValue.is) { @@ -198,7 +234,6 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - // Form text and user details val currentUser = AuthUser.currentUser val firstName = currentUser.map(_.firstName.get).getOrElse("") @@ -259,6 +294,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } + /** + * Fetches a consent by its ID. + * + * @return Box[MappedConsent] containing the consent if found. + */ private def callGetConsentByConsentId(): Box[MappedConsent] = { val requestParam = List( ObpS.param("CONSENT_ID"), @@ -271,11 +311,13 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } + /** + * Handles the confirmation of a consent request. + */ private def confirmConsentRequestProcess() = { - if(selectedAccountsIbansValue.is.isEmpty && + if (selectedAccountsIbansValue.is.isEmpty && selectedBalancesIbansValue.is.isEmpty && - selectedTransactionsIbansValue.is.isEmpty) - { + selectedTransactionsIbansValue.is.isEmpty) { S.error(s"Please select at least 1 account") } else { val consentId = ObpS.param("CONSENT_ID") openOr ("") @@ -286,8 +328,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" ) } - } + + /** + * Handles the denial of a consent request. + */ private def denyConsentRequestProcess() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") Consents.consentProvider.vend.updateConsentStatus(consentId, ConsentStatus.rejected) @@ -295,6 +340,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 s"$redirectUriValue?CONSENT_ID=${consentId}" ) } + + /** + * Handles the confirmation of a consent request with SCA (Strong Customer Authentication). + */ private def confirmConsentRequestProcessSca() = { val consentId = ObpS.param("CONSENT_ID") openOr ("") Consents.consentProvider.vend.getConsentByConsentId(consentId) match { @@ -308,7 +357,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } } - + /** + * Renders the SCA confirmation form for Berlin Group consent. + * + * @return CssSel for rendering the form. + */ def confirmBgConsentRequest: CssSel = { "#otp-value" #> SHtml.text(otpValue, otpValue(_)) & "type=submit" #> SHtml.onSubmitUnit(confirmConsentRequestProcessSca) From 1d80588a599ab9b1dbd61ce860dbc87c6b9578f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 13:41:50 +0100 Subject: [PATCH 15/23] docfix/Implement BG consent flow when user chooses accounts to link at confirmation step 2 --- .../code/snippet/BerlinGroupConsent.scala | 107 ++++++++++-------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala index f082b4455..59bcbfb57 100644 --- a/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala +++ b/obp-api/src/main/scala/code/snippet/BerlinGroupConsent.scala @@ -74,10 +74,9 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 super.set(value) } } - - private object selectedBalancesIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for balances - private object selectedTransactionsIbansValue extends SessionVar[Set[String]](Set()) // Stores selected IBANs for transactions - + private object accessAccountsDefinedVar extends SessionVar(true) + private object accessBalancesDefinedVar extends SessionVar(true) + private object accessTransactionsDefinedVar extends SessionVar(true) /** * Creates a ConsentAccessJson object from lists of IBANs for accounts, balances, and transactions. * @@ -93,8 +92,8 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 ConsentAccessJson( accounts = Some(accountsList), // Populate accounts - balances = Some(balancesList), // Populate balances - transactions = Some(transactionsList) // Populate transactions + balances = if (balancesList.nonEmpty) Some(balancesList) else None, // Populate balances + transactions = if (transactionsList.nonEmpty) Some(transactionsList) else None // Populate transactions ) } @@ -102,12 +101,10 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 * Updates the consent with new IBANs for accounts, balances, and transactions. * * @param consentId The ID of the consent to update. - * @param ibansAccount List of IBANs for accounts. - * @param ibansBalance List of IBANs for balances. - * @param ibansTransaction List of IBANs for transactions. + * @param ibans List of IBANs for accounts. * @return Future[MappedConsent] representing the updated consent. */ - private def updateConsent(consentId: String, ibansAccount: List[String], ibansBalance: List[String], ibansTransaction: List[String]): Future[MappedConsent] = { + private def updateConsent(consentId: String, ibans: List[String], canReadBalances: Boolean, canReadTransactions: Boolean): Future[MappedConsent] = { for { // Fetch the consent by ID consent: MappedConsent <- Future(Consents.consentProvider.vend.getConsentByConsentId(consentId)) map { @@ -115,7 +112,11 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 } // Update the consent JWT with new access details consentJWT <- Consent.updateAccountAccessOfBerlinGroupConsentJWT( - createConsentAccessJson(ibansAccount, ibansBalance, ibansTransaction), + createConsentAccessJson( + ibans, + if(canReadBalances) ibans else List(), + if(canReadTransactions) ibans else List() + ), consent, None ) map { @@ -161,40 +162,57 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 By(BankAccountRouting.AccountRoutingScheme, "IBAN") ).map(_.AccountRoutingAddress.get) } + // Select all IBANs + selectedAccountsIbansValue.set(userIbans) // Determine which IBANs the user can access for accounts, balances, and transactions val canReadAccountsIbans: List[String] = json.access.accounts match { - case Some(accounts) if accounts.isEmpty => + case Some(accounts) if accounts.isEmpty => // Access is requested updateConsentPayloadValue.set(true) + accessAccountsDefinedVar.set(true) userIbans.toList - case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => + case Some(accounts) if accounts.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessAccountsDefinedVar.set(true) accounts.flatMap(_.iban) - case Some(accounts) => + case Some(accounts) => // Logged in user is not an owner of IBAN/IBANs userIsOwnerOfAccountsValue.set(false) + accessAccountsDefinedVar.set(true) accounts.flatMap(_.iban) - case None => List() + case None => // Access is not requested + accessAccountsDefinedVar.set(false) + List() } val canReadBalancesIbans: List[String] = json.access.balances match { - case Some(balances) if balances.isEmpty => + case Some(balances) if balances.isEmpty => // Access is requested updateConsentPayloadValue.set(true) + accessBalancesDefinedVar.set(true) userIbans.toList - case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => + case Some(balances) if balances.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessBalancesDefinedVar.set(true) balances.flatMap(_.iban) - case Some(balances) => + case Some(balances) => // Logged in user is not an owner of IBAN/IBANs userIsOwnerOfAccountsValue.set(false) + accessBalancesDefinedVar.set(true) balances.flatMap(_.iban) - case None => List() + case None => // Access is not requested + accessBalancesDefinedVar.set(false) + List() } val canReadTransactionsIbans: List[String] = json.access.transactions match { - case Some(transactions) if transactions.isEmpty => + case Some(transactions) if transactions.isEmpty => // Access is requested updateConsentPayloadValue.set(true) + accessTransactionsDefinedVar.set(true) userIbans.toList - case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => + case Some(transactions) if transactions.flatMap(_.iban).toSet.subsetOf(userIbans) => // Access is requested for specific IBANs + accessTransactionsDefinedVar.set(true) transactions.flatMap(_.iban) - case Some(transactions) => + case Some(transactions) => // Logged in user is not an owner of IBAN/IBANs userIsOwnerOfAccountsValue.set(false) + accessTransactionsDefinedVar.set(true) transactions.flatMap(_.iban) - case None => List() + case None => // Access is not requested + accessTransactionsDefinedVar.set(false) + List() } /** @@ -253,28 +271,20 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510

{formTextHtml}

- -

1) Read account (basic) details of:

-
- {generateCheckboxes("canReadAccountsIbans", canReadAccountsIbans, selectedAccountsIbansValue.is, selectedAccountsIbansValue)} +
+

Allowed actions:

+

Read account details

+

Read account balances

+

Read transactions

-
- -

2) Read account balances of:

-
- {generateCheckboxes("canReadBalancesIbans", canReadBalancesIbans, selectedBalancesIbansValue.is, selectedBalancesIbansValue)} +
+

Accounts:

+
+ {generateCheckboxes("canReadAccountsIbans", userIbans.toList, selectedAccountsIbansValue.is, selectedAccountsIbansValue)} +
+
-
- -

3) Read transactions of:

-
- {generateCheckboxes("canReadTransactionsIbans", canReadTransactionsIbans, selectedTransactionsIbansValue.is, selectedTransactionsIbansValue)} -
-
- -

This consent will end on date - {json.validUntil} - .

+

This consent will end on date {json.validUntil}.

I understand that I can revoke this consent at any time.

) & { @@ -315,14 +325,17 @@ class BerlinGroupConsent extends MdcLoggable with RestHelper with APIMethods510 * Handles the confirmation of a consent request. */ private def confirmConsentRequestProcess() = { - if (selectedAccountsIbansValue.is.isEmpty && - selectedBalancesIbansValue.is.isEmpty && - selectedTransactionsIbansValue.is.isEmpty) { + if (selectedAccountsIbansValue.is.isEmpty) { S.error(s"Please select at least 1 account") } else { val consentId = ObpS.param("CONSENT_ID") openOr ("") if (updateConsentPayloadValue.is) { - updateConsent(consentId, selectedAccountsIbansValue.is.toList, selectedBalancesIbansValue.is.toList, selectedTransactionsIbansValue.is.toList) + updateConsent( + consentId, + selectedAccountsIbansValue.is.toList, + accessBalancesDefinedVar.is, + accessTransactionsDefinedVar.is + ) } S.redirectTo( s"/confirm-bg-consent-request-sca?CONSENT_ID=${consentId}" From 9f6707e28a4e9d052522f259e4e8cedf9d053605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 13:54:00 +0100 Subject: [PATCH 16/23] docfix/Remove non standard chars --- .../main/scala/code/api/util/CertificateVerifier.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 848c4efc5..b61053b9e 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -40,10 +40,10 @@ object CertificateVerifier extends MdcLoggable { trustStore } match { case Success(store) => - logger.info(s"✅ Loaded trust store from: $trustStorePath") + logger.info(s"Loaded trust store from: $trustStorePath") Some(store) case Failure(e) => - logger.info(s"❌ Failed to load trust store: ${e.getMessage}") + logger.info(s"Failed to load trust store: ${e.getMessage}") None } } @@ -145,11 +145,11 @@ object CertificateVerifier extends MdcLoggable { pemCertificate.foreach { pem => val isValid = validateCertificate(pem) - logger.info(s"✅ Certificate verification result: $isValid") + logger.info(s"Certificate verification result: $isValid") } loadTrustStore().foreach { trustStore => - logger.info(s"🔹 Trust Store contains ${trustStore.size()} certificates.") + logger.info(s"Trust Store contains ${trustStore.size()} certificates.") } } } From d9f8c06096899df48c1868d4d10fddd4da9d6c47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:42:23 +0100 Subject: [PATCH 17/23] docfix/Remove non standard chars 2 --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index b61053b9e..1773cab7e 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -133,7 +133,7 @@ object CertificateVerifier extends MdcLoggable { } match { case Success(pem) => Some(pem) case Failure(exception) => - println(s"❌ Failed to load PEM certificate from file: ${exception.getMessage}") + logger.error(s"Failed to load PEM certificate from file: ${exception.getMessage}") None } } From 0ba5b7b4934d056326ce8c5616cc473bff7d4efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:43:37 +0100 Subject: [PATCH 18/23] docfix/Remove non standard chars 3 --- .../src/main/scala/code/api/util/CertificateVerifier.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 1773cab7e..adbdd56ae 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -91,15 +91,15 @@ object CertificateVerifier extends MdcLoggable { val validator = CertPathValidator.getInstance("PKIX") validator.validate(certPath, pkixParams) - logger.info("✅ Certificate is valid and trusted.") + logger.info("Certificate is valid and trusted.") true } match { case Success(_) => true case Failure(e: CertPathValidatorException) => - logger.info(s"❌ Certificate validation failed: ${e.getMessage}") + logger.info(s"Certificate validation failed: ${e.getMessage}") false case Failure(e) => - logger.info(s"❌ Error: ${e.getMessage}") + logger.info(s"Error: ${e.getMessage}") false } } From f9effa8a6e99fbdcf6f4ed7bca3350740ae30568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:45:06 +0100 Subject: [PATCH 19/23] docfix/Remove non standard chars 4 --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index adbdd56ae..9204d08c9 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -19,8 +19,6 @@ object CertificateVerifier extends MdcLoggable { * This function: * - Reads the trust store password from the application properties (`truststore.path.tpp_signature`). * - Uses Java's `KeyStore` class to load the certificates. - * - If successful, logs `✅ Loaded trust store from: path`. - * - If it fails, logs `❌ Failed to load trust store: error message`. * * @return An `Option[KeyStore]` containing the loaded trust store, or `None` if loading fails. */ From c667f79b222a67515bb21247c00cdf6a079a9079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Tue, 25 Feb 2025 15:48:40 +0100 Subject: [PATCH 20/23] docfix/Remove personal path value --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 9204d08c9..83a99fdcd 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -137,8 +137,7 @@ object CertificateVerifier extends MdcLoggable { } def main(args: Array[String]): Unit = { - // val certificatePath = "/path/to/certificate.pem" - val certificatePath = "/home/marko/Downloads/BerlinGroupSigning/certificate.pem" + val certificatePath = "/path/to/certificate.pem" val pemCertificate = loadPemCertificateFromFile(certificatePath) pemCertificate.foreach { pem => From 3eabc5903cfdda343569c76890e331374317747e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 26 Feb 2025 14:07:28 +0100 Subject: [PATCH 21/23] docfix/Add some comments --- obp-api/src/main/scala/code/api/util/CertificateVerifier.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala index 83a99fdcd..cefb24abe 100644 --- a/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala +++ b/obp-api/src/main/scala/code/api/util/CertificateVerifier.scala @@ -137,6 +137,7 @@ object CertificateVerifier extends MdcLoggable { } def main(args: Array[String]): Unit = { + // change the following path if using this function to test on your localhost val certificatePath = "/path/to/certificate.pem" val pemCertificate = loadPemCertificateFromFile(certificatePath) From e6598cc2299e021e1bbee7ec57cbaaa3c3af0f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 26 Feb 2025 15:14:57 +0100 Subject: [PATCH 22/23] docfix/Fix the sandbox example data --- .../2016-04-28/example_import.json | 83 ++++++++++--------- .../sandbox/example_data/example_import.json | 75 +++++++++-------- 2 files changed, 84 insertions(+), 74 deletions(-) diff --git a/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json b/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json index 640c91a4d..f6ee011d8 100644 --- a/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json +++ b/obp-api/src/main/scala/code/api/sandbox/example_data/2016-04-28/example_import.json @@ -12,47 +12,48 @@ "logo":"https://static.openbankproject.com/images/sandbox/bank_y.png", "website":"https://www.example.com" }], - "users":[{ - "email":"robert.xuk.x@example.com", - "user_name": "robert.xuk.x@example.com", - "password":"5232e7", - "display_name":"Robert XUk X" - },{ - "email":"susan.xuk.x@example.com", - "user_name": "susan.xuk.x@example.com", - "password":"43ca4d", - "display_name":"Susan XUk X" - },{ - "email":"anil.xuk.x@example.com", - "user_name": "anil.xuk.x@example.com", - "password":"d8c716", - "display_name":"Anil XUk X" - },{ - "email":"ellie.xuk.x@example.com", - "user_name": "ellie.xuk.x@example.com", - "password":"6187b9", - "display_name":"Ellie XUk X" - },{ - "email":"robert.yuk.y@example.com", - "user_name": "robert.yuk.y@example.com", - "password":"e5046a", - "display_name":"Robert YUk Y" - },{ - "email":"susan.yuk.y@example.com", - "user_name": "susan.yuk.y@example.com", - "password":"5b38a6", - "display_name":"Susan YUk Y" - },{ - "email":"anil.yuk.y@example.com", - "user_name": "anil.yuk.y@example.com", - "password":"dcf03d", - "display_name":"Anil YUk Y" - },{ - "email":"ellie.yuk.y@example.com", - "user_name": "ellie.yuk.y@example.com", - "password":"4f9eaa", - "display_name":"Ellie YUk Y" - }], + "users": [ + { + "email": "robert.x.0.gh@example.com", + "password": "V8%Ktssl(L", + "user_name": "Robert.X.0.GH" + }, + { + "email": "susan.x.0.gh@example.com", + "password": "naW9u3C%bh", + "user_name": "Susan.X.0.GH" + }, + { + "email": "anil.x.0.gh@example.com", + "password": "9W0RIrX-6f", + "user_name": "Anil.X.0.GH" + }, + { + "email": "ellie.x.0.gh@example.com", + "password": "rMf_OHM0dW", + "user_name": "Ellie.X.0.GH" + }, + { + "email": "robert.y.9.gh@example.com", + "password": "%1Z43kzt2L", + "user_name": "Robert.Y.9.GH" + }, + { + "email": "susan.y.9.gh@example.com", + "password": "oITehM!B2V", + "user_name": "Susan.Y.9.GH" + }, + { + "email": "anil.y.9.gh@example.com", + "password": "TuKaNO8oI-", + "user_name": "Anil.Y.9.GH" + }, + { + "email": "ellie.y.9.gh@example.com", + "password": "SkJDH+ds2_", + "user_name": "Ellie.Y.9.GH" + } + ], "accounts":[{ "id":"05237266-b334-4704-a087-5b460a2ecf04", "bank":"psd201-bank-x--uk", diff --git a/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json b/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json index 91418c4d6..0126ddd05 100644 --- a/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json +++ b/obp-api/src/main/scala/code/api/sandbox/example_data/example_import.json @@ -12,39 +12,48 @@ "logo":"https://static.openbankproject.com/images/sandbox/bank_y.png", "website":"https://www.example.com" }], - "users":[{ - "email":"robert.x.0.gh@example.com", - "password":"X!d1edcafd", - "user_name":"Robert.X.0.GH" - },{ - "email":"susan.x.0.gh@example.com", - "password":"X!90e4e3e4", - "user_name":"Susan.X.0.GH" - },{ - "email":"anil.x.0.gh@example.com", - "password":"X!eb06b005", - "user_name":"Anil.X.0.GH" - },{ - "email":"ellie.x.0.gh@example.com", - "password":"X!5bc94405", - "user_name":"Ellie.X.0.GH" - },{ - "email":"robert.y.9.gh@example.com", - "password":"X!039941de", - "user_name":"Robert.Y.9.GH" - },{ - "email":"susan.y.9.gh@example.com", - "password":"X!bb4efa3d", - "user_name":"Susan.Y.9.GH" - },{ - "email":"anil.y.9.gh@example.com", - "password":"X!098915cd", - "user_name":"Anil.Y.9.GH" - },{ - "email":"ellie.y.9.gh@example.com", - "password":"X!6170b37b", - "user_name":"Ellie.Y.9.GH" - }], + "users": [ + { + "email": "robert.x.0.gh@example.com", + "password": "V8%Ktssl(L", + "user_name": "Robert.X.0.GH" + }, + { + "email": "susan.x.0.gh@example.com", + "password": "naW9u3C%bh", + "user_name": "Susan.X.0.GH" + }, + { + "email": "anil.x.0.gh@example.com", + "password": "9W0RIrX-6f", + "user_name": "Anil.X.0.GH" + }, + { + "email": "ellie.x.0.gh@example.com", + "password": "rMf_OHM0dW", + "user_name": "Ellie.X.0.GH" + }, + { + "email": "robert.y.9.gh@example.com", + "password": "%1Z43kzt2L", + "user_name": "Robert.Y.9.GH" + }, + { + "email": "susan.y.9.gh@example.com", + "password": "oITehM!B2V", + "user_name": "Susan.Y.9.GH" + }, + { + "email": "anil.y.9.gh@example.com", + "password": "TuKaNO8oI-", + "user_name": "Anil.Y.9.GH" + }, + { + "email": "ellie.y.9.gh@example.com", + "password": "SkJDH+ds2_", + "user_name": "Ellie.Y.9.GH" + } + ], "accounts":[{ "id":"f65e28a5-9abe-428f-85bb-6c3c38122adb", "bank":"obp-bank-x-gh", From c7eb718956336249050a348a8c9ee5d17675ceb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Mili=C4=87?= Date: Wed, 26 Feb 2025 15:15:36 +0100 Subject: [PATCH 23/23] feature/Add more logging regarding import sandbox data --- obp-api/src/main/scala/code/sandbox/OBPDataImport.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala index 22e366afc..0024598c0 100644 --- a/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala +++ b/obp-api/src/main/scala/code/sandbox/OBPDataImport.scala @@ -72,7 +72,9 @@ trait OBPDataImport extends MdcLoggable { protected def dataOrFirstFailure[T](boxes : List[Box[T]]) = { val firstFailure = boxes.collectFirst{case f: Failure => f} firstFailure match { - case Some(f) => f + case Some(f) => + logger.debug(f) + f case None => Full(boxes.flatten) //no failures, so we can return the results } }