gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated (5ba09a22 -> 7dfef458)


From: gnunet
Subject: [libeufin] branch master updated (5ba09a22 -> 7dfef458)
Date: Sat, 07 Jan 2023 13:48:59 +0100

This is an automated email from the git hooks/post-receive script.

ms pushed a change to branch master
in repository libeufin.

    from 5ba09a22 Test Access API's wire transfer.
     new 03368f41 readme
     new 121235aa comment
     new b45cb5da Fix accidental import.
     new 95d4c8e6 Nexus DB helper to get bank accounts.
     new 14cdc271 Nexus HTTP status codes review.
     new 7d2c532d Nexus HTTP status codes review.
     new f23911ed Introduce debit check helper.
     new 37725bb8 Debit check at server side EBICS.
     new 105bed29 fix loglevel
     new 7dfef458 Test debit detection when serving EBICS.

The 10 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 README                                             |  8 +++-
 cli/bin/circuit_test.sh                            |  4 +-
 nexus/build.gradle                                 |  2 +-
 .../tech/libeufin/nexus/bankaccount/BankAccount.kt | 15 ++++++-
 .../tech/libeufin/nexus/ebics/EbicsClient.kt       | 51 ++++++++++++++--------
 .../tech/libeufin/nexus/server/NexusServer.kt      | 13 +++---
 nexus/src/test/kotlin/DownloadAndSubmit.kt         | 25 +++++++++++
 nexus/src/test/kotlin/MakeEnv.kt                   | 12 +++++
 .../tech/libeufin/sandbox/EbicsProtocolBackend.kt  | 27 ++++++++++--
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  |  2 +-
 .../kotlin/tech/libeufin/sandbox/bankAccount.kt    | 51 +++++++++++++---------
 util/src/main/kotlin/Ebics.kt                      |  1 +
 12 files changed, 153 insertions(+), 58 deletions(-)

diff --git a/README b/README
index fc9fc1fa..0ea44866 100644
--- a/README
+++ b/README
@@ -3,14 +3,13 @@ Installing LibEuFin
 
 $ ./bootstrap
 $ ./configure --prefix=$PFX
-$ make install # Note: This may require Java=18; Java=17 broke for me, 
Java>=19 is unsupported by gradle
+$ make install # Note: This may require Java=18; Java=17 had errors, Java>=19 
is unsupported by gradle
 
 If the previous step succeeded, libeufin-nexus and a command line
 client (libeufin-cli) should be found under $PFX/bin.  Additionally,
 the libeufin-sandbox command used for testing should be found under
 $PFX/bin as well.
 
-
 Launching LibEuFin
 ==================
 
@@ -18,6 +17,11 @@ Launch Nexus:
 
 $ libeufin-nexus serve 
--with-db=jdbc:postgres://localhost:5433?user=foo&password=bar
 
+More instructions about configuring and setting Libeufin
+are available at this link:
+
+https://docs.taler.net/libeufin/nexus-tutorial.html
+
 Exporting a dist-file
 =====================
 
diff --git a/cli/bin/circuit_test.sh b/cli/bin/circuit_test.sh
index 997351cc..b05f79f3 100755
--- a/cli/bin/circuit_test.sh
+++ b/cli/bin/circuit_test.sh
@@ -5,15 +5,15 @@
 
 set -eu
 
+echo TESTING THE CLI SIDE OF THE CIRCUIT API
 jq --version &> /dev/null || (echo "'jq' command not found"; exit 77)
 curl --version &> /dev/null || (echo "'curl' command not found"; exit 77)
 
 DB_PATH=/tmp/circuit-test.sqlite3
 export LIBEUFIN_SANDBOX_DB_CONNECTION=jdbc:sqlite:$DB_PATH
+# NOTE: unset this variable to test the SMS or e-mail TAN.
 export LIBEUFIN_CASHOUT_TEST_TAN=secret-tan
 
-echo TESTING THE CLI SIDE OF THE CIRCUIT API
-
 echo -n Delete previous data..
 rm -f $DB_PATH
 echo DONE
diff --git a/nexus/build.gradle b/nexus/build.gradle
index 75f4254c..4942ecce 100644
--- a/nexus/build.gradle
+++ b/nexus/build.gradle
@@ -49,7 +49,6 @@ dependencies {
 
     // LibEuFin util library
     implementation project(":util")
-    implementation project(":sandbox") // for testing
 
     // Logging
     implementation 'ch.qos.logback:logback-classic:1.2.5'
@@ -97,6 +96,7 @@ dependencies {
     testImplementation 'org.junit.jupiter:junit-jupiter:5.7.1'
     testImplementation 'org.jetbrains.kotlin:kotlin-test:1.5.21'
     testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
+    testImplementation project(":sandbox")
 }
 
 test {
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index 6cd21c62..af72ab1f 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -352,7 +352,20 @@ fun getPaymentInitiation(uuid: Long): 
PaymentInitiationEntity {
         "Payment '$uuid' not found"
     )
 }
-
+fun getBankAccount(label: String): NexusBankAccountEntity {
+    val maybeBankAccount = transaction {
+        NexusBankAccountEntity.findByName(label)
+    }
+    return maybeBankAccount ?:
+    throw NexusError(
+        HttpStatusCode.NotFound,
+        "Account $label not found"
+    )
+}
+fun addPaymentInitiation(paymentData: Pain001Data, debtorAccount: String): 
PaymentInitiationEntity {
+    val bankAccount = getBankAccount(debtorAccount)
+    return addPaymentInitiation(paymentData, bankAccount)
+}
 
 /**
  * Insert one row in the database, and leaves it marked as non-submitted.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
index 136bf7e8..ffaefd4f 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -46,8 +46,12 @@ private suspend inline fun HttpClient.postToBank(url: 
String, body: String): Str
         }
     } catch (e: ClientRequestException) {
         logger.error(e.message)
+        val returnStatus = if (e.response.status.value == 
HttpStatusCode.RequestTimeout.value)
+            HttpStatusCode.GatewayTimeout
+        else HttpStatusCode.BadGateway
+        
         throw NexusError(
-            HttpStatusCode.BadGateway,
+            returnStatus,
             e.message
         )
     }
@@ -78,7 +82,7 @@ class EbicsDownloadSuccessResult(
 ) : EbicsDownloadResult()
 
 /**
- * Some bank-technical error occured.
+ * A bank-technical error occurred.
  */
 class EbicsDownloadBankErrorResult(
     val returnCode: EbicsReturnCode
@@ -106,8 +110,12 @@ suspend fun doEbicsDownloadTransaction(
             // Success, nothing to do!
         }
         else -> {
+            /**
+             * The bank gave a valid response that contains however
+             * an error.  Hence Nexus sent not processable instructions. */
+
             throw EbicsProtocolError(
-                HttpStatusCode.InternalServerError,
+                HttpStatusCode.UnprocessableEntity,
                 "unexpected return code ${initResponse.technicalReturnCode}",
                 initResponse.technicalReturnCode
             )
@@ -126,16 +134,19 @@ suspend fun doEbicsDownloadTransaction(
 
     val transactionID =
         initResponse.transactionID ?: throw NexusError(
-            HttpStatusCode.InternalServerError,
+            HttpStatusCode.BadGateway,
             "Initial response must contain transaction ID"
         )
     logger.debug("Bank acknowledges EBICS download initialization.  
Transaction ID: $transactionID.")
     val encryptionInfo = initResponse.dataEncryptionInfo
-        ?: throw NexusError(HttpStatusCode.InternalServerError, "initial 
response did not contain encryption info")
+        ?: throw NexusError(
+            HttpStatusCode.BadGateway,
+            "initial response did not contain encryption info"
+        )
 
     val initOrderDataEncChunk = initResponse.orderDataEncChunk
         ?: throw NexusError(
-            HttpStatusCode.InternalServerError,
+            HttpStatusCode.BadGateway,
             "initial response for download transaction does not contain data 
transfer"
         )
 
@@ -173,7 +184,7 @@ suspend fun doEbicsDownloadTransaction(
         }
         val transferOrderDataEncChunk = transferResponse.orderDataEncChunk
             ?: throw NexusError(
-                HttpStatusCode.InternalServerError,
+                HttpStatusCode.BadGateway,
                 "transfer response for download transaction does not contain 
data transfer"
             )
         payloadChunks.add(transferOrderDataEncChunk)
@@ -222,18 +233,22 @@ suspend fun doEbicsUploadTransaction(
     val responseStr = client.postToBank(subscriberDetails.ebicsUrl, req)
 
     val initResponse = parseAndValidateEbicsResponse(subscriberDetails, 
responseStr)
+    // The bank indicated one error, hence Nexus sent invalid data.
     if (initResponse.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
-        throw NexusError(HttpStatusCode.InternalServerError, reason = 
"unexpected return code")
-    }
-
-    val transactionID =
-        initResponse.transactionID ?: throw NexusError(
+        throw NexusError(
             HttpStatusCode.InternalServerError,
+            reason = "unexpected return code"
+        )
+    }
+    // The bank did NOT indicate any error, but the response
+    // lacks required information, blame the bank.
+    val transactionID = initResponse.transactionID ?: throw NexusError(
+            HttpStatusCode.BadGateway,
             "init response must have transaction ID"
         )
     logger.debug("Bank acknowledges EBICS upload initialization.  Transaction 
ID: $transactionID.")
-    /* now send actual payload */
 
+    /* now send actual payload */
     val ebicsPayload = createEbicsRequestForUploadTransferPhase(
         subscriberDetails,
         transactionID,
@@ -251,11 +266,11 @@ suspend fun doEbicsUploadTransaction(
         else -> {
             throw EbicsProtocolError(
                 /**
-                 * 500 because Nexus walked until having the
-                 * bank rejecting the operation instead of it
-                 * detecting the problem.
-                 */
-                httpStatusCode = HttpStatusCode.InternalServerError,
+                 * The communication was valid, but the content may have
+                 * caused a problem in the bank.  Nexus MAY but it's not 
required
+                 * to check all possible business conditions before requesting
+                 * to the bank. */
+                httpStatusCode = HttpStatusCode.UnprocessableEntity,
                 reason = txResp.reportText,
                 ebicsTechnicalCode = txResp.technicalReturnCode
             )
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index 23df07a5..7dceecc5 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -52,8 +52,6 @@ import tech.libeufin.nexus.*
 import tech.libeufin.nexus.bankaccount.*
 import tech.libeufin.nexus.ebics.*
 import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
-import tech.libeufin.sandbox.SandboxErrorDetailJson
-import tech.libeufin.sandbox.SandboxErrorJson
 import tech.libeufin.util.*
 import java.net.BindException
 import java.net.URLEncoder
@@ -214,14 +212,13 @@ val nexusApp: Application.() -> Unit = {
             )
         }
         exception<BadRequestException> { call, cause ->
-            tech.libeufin.sandbox.logger.error("Exception while handling 
'${call.request.uri}', ${cause.message}")
+            logger.error("Exception while handling '${call.request.uri}', 
${cause.message}")
             call.respond(
                 HttpStatusCode.BadRequest,
-                SandboxErrorJson(
-                    error = SandboxErrorDetailJson(
-                        type = "util-error",
-                        description = cause.message ?: "Bad request but did 
not find exact cause."
-                    )
+                ErrorResponse(
+                    code = 
TalerErrorCode.TALER_EC_LIBEUFIN_NEXUS_GENERIC_ERROR.code,
+                    detail = cause.message ?: "Bad request but did not find 
exact cause.",
+                    hint = "Malformed request or unacceptable values"
                 )
             )
         }
diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt 
b/nexus/src/test/kotlin/DownloadAndSubmit.kt
index 3d51dc45..0b8b5914 100644
--- a/nexus/src/test/kotlin/DownloadAndSubmit.kt
+++ b/nexus/src/test/kotlin/DownloadAndSubmit.kt
@@ -301,4 +301,29 @@ class DownloadAndSubmit {
             }
         }
     }
+
+    // Test the EBICS error message in case of debt threshold being surpassed
+    @Test
+    fun testDebit() {
+        withNexusAndSandboxUser {
+            testApplication {
+                application(sandboxApp)
+                runBlocking {
+                    // Create Pain.001 with excessive amount.
+                    addPaymentInitiation(
+                        Pain001Data(
+                            creditorIban = getIban(),
+                            creditorBic = "SANDBOXX",
+                            creditorName = "Tester",
+                            subject = "test payment",
+                            sum = "1000000",
+                            currency = "TESTKUDOS"
+                        ),
+                        "foo"
+                    )
+                    assertException<EbicsProtocolError> { 
submitAllPaymentInitiations(client, "foo") }
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
index a6e2ce2d..238b912c 100644
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ b/nexus/src/test/kotlin/MakeEnv.kt
@@ -31,6 +31,18 @@ val userKeys = EbicsKeys(
     enc = CryptoUtil.generateRsaKeyPair(2048),
     sig = CryptoUtil.generateRsaKeyPair(2048)
 )
+
+// New versions of JUnit provide this!
+inline fun <reified ExceptionType> assertException(block: () -> Unit) {
+    try {
+        block()
+    } catch (e: Throwable) {
+        assert(e.javaClass == ExceptionType::class.java)
+        return
+    }
+    return assert(false)
+}
+
 /**
  * Run a block after connecting to the test database.
  * Cleans up the DB file afterwards.
diff --git 
a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
index 7ab5696c..c2e235c5 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
@@ -67,7 +67,7 @@ data class PainParseResult(
 open class EbicsRequestError(
     val errorText: String,
     val errorCode: String
-) : Exception("$errorText ($errorCode)")
+) : Exception("$errorText (EBICS error code: $errorCode)")
 
 class EbicsNoDownloadDataAvailable(reason: String? = null) : EbicsRequestError(
     "[EBICS_NO_DOWNLOAD_DATA_AVAILABLE]" + if (reason != null) " $reason" else 
"",
@@ -126,6 +126,11 @@ class EbicsProcessingError(detail: String) : 
EbicsRequestError(
     "091116"
 )
 
+class EbicsAmountCheckError(detail: String): EbicsRequestError(
+    "[EBICS_AMOUNT_CHECK_FAILED] $detail",
+    "091303"
+)
+
 suspend fun respondEbicsTransfer(
     call: ApplicationCall,
     errorText: String,
@@ -697,8 +702,12 @@ private fun parsePain001(paymentRequest: String): 
PainParseResult {
 }
 
 /**
- * Process a payment request in the pain.001 format.
- */
+ * Process a payment request in the pain.001 format.  Note:
+ * the receiver IBAN is NOT checked to have one account at
+ * the Sandbox.  That's because (1) it leaves open to send
+ * payments outside of the running Sandbox and (2) may ease
+ * tests where the preparation logic can skip creating also
+ * the receiver account.  */
 private fun handleCct(paymentRequest: String,
                       requestingSubscriber: EbicsSubscriberEntity
 ) {
@@ -731,7 +740,17 @@ private fun handleCct(paymentRequest: String,
             "[EBICS_PROCESSING_ERROR] Currency (${parseResult.currency}) not 
supported.",
             "091116"
         )
-        // FIXME: check that debtor IBAN _is_ the requesting subscriber.
+        // Check for the debit case.
+        val maybeAmount = try {
+            BigDecimal(parseResult.amount)
+        } catch (e: Exception) {
+            logger.warn("Although PAIN validated, BigDecimal didn't parse its 
amount (${parseResult.amount})!")
+            throw EbicsProcessingError("The CCT request contains an invalid 
amount: ${parseResult.amount}")
+        }
+        if (maybeDebit(bankAccount.label, maybeAmount))
+            throw EbicsAmountCheckError("The requested amount 
(${parseResult.amount}) would exceed the debit threshold")
+
+        // Get the two parties.
         BankAccountTransactionEntity.new {
             account = bankAccount
             demobank = bankAccount.demoBank
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index ca4cb8ff..f514ae60 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -579,7 +579,7 @@ val sandboxApp: Application.() -> Unit = {
             )
         }
         exception<EbicsRequestError> { call, cause ->
-            logger.info("Handling EbicsRequestError: ${cause.message}")
+            logger.error("Handling EbicsRequestError: ${cause.message}")
             respondEbicsTransfer(call, cause.errorText, cause.errorCode)
         }
     }
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
index eacae3c3..50963695 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
@@ -6,6 +6,32 @@ import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.util.*
 import java.math.BigDecimal
 
+/**
+ * Check whether the given bank account would surpass the
+ * debit threshold, in case the potential amount gets transferred.
+ * Returns true when the debit WOULD be surpassed.  */
+fun maybeDebit(
+    accountLabel: String,
+    requestedAmount: BigDecimal,
+    demobankName: String = "default"
+): Boolean {
+    val demobank = getDemobank(demobankName) ?: throw notFound(
+        "Demobank '${demobankName}' not found when trying to check the debit 
threshold" +
+                " for user $accountLabel"
+    )
+    val balance = getBalance(accountLabel, withPending = true)
+    val maxDebt = if (accountLabel == "admin") {
+        demobank.bankDebtLimit
+    } else demobank.usersDebtLimit
+    val balanceCheck = balance - requestedAmount
+    if (balanceCheck < BigDecimal.ZERO && balanceCheck.abs() > 
BigDecimal.valueOf(maxDebt.toLong())) {
+        logger.warn("User '$accountLabel' would surpass the debit" +
+                " threshold of $maxDebt, given the requested amount of 
${requestedAmount.toPlainString()}")
+        return true
+    }
+    return false
+}
+
 /**
  * The last balance is the one mentioned in the bank account's
  * last statement.  If the bank account does not have any statement
@@ -54,7 +80,7 @@ fun getBalance(
 }
 
 // Wrapper offering to get bank accounts from a string.
-fun getBalance(accountLabel: String, withPending: Boolean = false): BigDecimal 
{
+fun getBalance(accountLabel: String, withPending: Boolean = true): BigDecimal {
     val defaultDemobank = getDefaultDemobank()
     val account = getBankAccountFromLabel(accountLabel, defaultDemobank)
     return getBalance(account, withPending)
@@ -90,16 +116,8 @@ fun wireTransfer(
         pmtInfId
     )
 }
-/**
- * Book a CRDT and a DBIT transaction and return the unique reference thereof.
- *
- * At the moment there is redundancy because all the creditor / debtor details
- * are contained (directly or indirectly) already in the BankAccount 
parameters.
- *
- * This is kept both not to break the existing tests and to allow future 
versions
- * where one party of the transaction is not a customer of the running Sandbox.
- */
 
+// Book a CRDT and a DBIT transaction and return the unique reference thereof.
 fun wireTransfer(
     debitAccount: BankAccountEntity,
     creditAccount: BankAccountEntity,
@@ -118,17 +136,8 @@ fun wireTransfer(
                     "  Only ${demobank.currency} allowed."
         )
     // Check funds are sufficient.
-    /**
-     * Using 'pending' balance because Libeufin never books.  The
-     * reason is that booking is not Taler-relevant.
-     */
-    val pendingBalance = getBalance(debitAccount, withPending = true)
-    val maxDebt = if (debitAccount.label == "admin") {
-        demobank.bankDebtLimit
-    } else demobank.usersDebtLimit
-    val balanceCheck = pendingBalance - amountAsNumber
-    if (balanceCheck < BigDecimal.ZERO && balanceCheck.abs() > 
BigDecimal.valueOf(maxDebt.toLong())) {
-        logger.info("Account ${debitAccount.label} would surpass debit 
threshold of $maxDebt.  Rollback wire transfer")
+    if (maybeDebit(debitAccount.label, amountAsNumber)) {
+        logger.error("Account ${debitAccount.label} would surpass debit 
threshold.  Rollback wire transfer")
         throw SandboxError(HttpStatusCode.PreconditionFailed, "Insufficient 
funds")
     }
     val timeStamp = getUTCnow().toInstant().toEpochMilli()
diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt
index 6549dea5..182a02bf 100644
--- a/util/src/main/kotlin/Ebics.kt
+++ b/util/src/main/kotlin/Ebics.kt
@@ -366,6 +366,7 @@ enum class EbicsReturnCode(val errorCode: String) {
     EBICS_INVALID_USER_OR_USER_STATE("091002"),
     EBICS_PROCESSING_ERROR("091116"),
     EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"),
+    EBICS_AMOUNT_CHECK_FAILED("091303"),
     EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005");
 
     companion object {

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]