gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (252d91c5 -> 35e3c131)


From: gnunet
Subject: [libeufin] branch master updated (252d91c5 -> 35e3c131)
Date: Thu, 05 Jan 2023 17:05:23 +0100

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

ms pushed a change to branch master
in repository libeufin.

    from 252d91c5 CLI side of the Circuit API.
     new 571b2f9c UUID sanitization.
     new f1e1f636 Adding cash-out operations to the CLI.
     new 35e3c131 Circuit API.

The 3 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:
 cli/bin/circuit_test.sh                            | 112 +++++++++++
 cli/bin/libeufin-cli                               | 216 +++++++++++++++++++--
 .../kotlin/tech/libeufin/sandbox/CircuitApi.kt     |  37 +++-
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  |  43 ++--
 4 files changed, 358 insertions(+), 50 deletions(-)
 create mode 100755 cli/bin/circuit_test.sh

diff --git a/cli/bin/circuit_test.sh b/cli/bin/circuit_test.sh
new file mode 100755
index 00000000..b8e10fa3
--- /dev/null
+++ b/cli/bin/circuit_test.sh
@@ -0,0 +1,112 @@
+#!/bin/bash
+
+# Tests successful cases of the CLI acting
+# as the client of the Circuit API.
+
+set -eu
+
+DB_PATH=/tmp/circuit-test.sqlite3
+export LIBEUFIN_SANDBOX_DB_CONNECTION=jdbc:sqlite:$DB_PATH
+export LIBEUFIN_CASHOUT_TEST_TAN=secret-tan
+
+echo -n Delete previous data..
+rm -f $DB_PATH
+echo DONE
+echo -n Configure the default demobank...
+libeufin-sandbox config default
+echo DONE
+echo -n Start the bank...
+libeufin-sandbox serve &> sandbox.log &
+SANDBOX_PID=$!
+trap "echo -n 'killing the bank (pid $SANDBOX_PID)...'; kill $SANDBOX_PID; 
wait; echo DONE" EXIT
+echo DONE
+echo -n Wait for the bank...
+curl --max-time 2 --retry-connrefused --retry-delay 1 --retry 10 
http://localhost:5000/ &> /dev/null
+echo DONE
+echo Ask Circuit API /config...
+curl http://localhost:5000/demobanks/default/circuit-api/config &> /dev/null
+echo DONE
+echo -n "Register new account..."
+export LIBEUFIN_SANDBOX_USERNAME=admin
+export LIBEUFIN_SANDBOX_PASSWORD=secret
+export LIBEUFIN_NEW_CIRCUIT_ACCOUNT_PASSWORD=foo
+./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-register --name eee --username www \
+    --cashout-address payto://iban/FIAT --internal-iban LOCAL 
+echo DONE
+echo -n Reconfigure account specifying a phone number..
+# Give phone number.
+export LIBEUFIN_SANDBOX_USERNAME=www
+export LIBEUFIN_SANDBOX_PASSWORD=foo
+./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-reconfig --cashout-address payto://iban/WWW --phone +999
+echo DONE
+echo -n Create a cash-out operation...
+CASHOUT_RESP=$(./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-cashout --amount-debit=EUR:1 --amount-credit=CHF:0.95)
+echo DONE
+echo -n Extract the cash-out UUID...
+CASHOUT_UUID=$(echo ${CASHOUT_RESP} | jq --raw-output '.uuid')
+echo DONE
+echo -n Get cash-out details...
+RESP=$(./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-cashout-details \
+  --uuid $CASHOUT_UUID
+)
+OPERATION_STATUS=$(echo $RESP | jq --raw-output '.status')
+if ! test "$OPERATION_STATUS" = "PENDING"; then
+    echo Unexpected cash-out operation status found: $OPERATION_STATUS
+    exit 1
+fi
+echo DONE
+echo -n Delete the cash-out operation...
+RESP=$(./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-cashout-abort \
+  --uuid $CASHOUT_UUID
+)
+echo DONE
+echo -n Create another cash-out operation...
+CASHOUT_RESP=$(./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-cashout --amount-debit=EUR:1 --amount-credit=CHF:0.95)
+CASHOUT_UUID=$(echo ${CASHOUT_RESP} | jq --raw-output '.uuid')
+echo DONE
+echo -n Confirm the last cash-out operation...
+./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-cashout-confirm --uuid $CASHOUT_UUID --tan secret-tan
+echo DONE
+# The user now has -1 balance.  Let the bank
+# award EUR:1 to them, in order to bring their
+# balance to zero.
+echo -n Bring the account to 0 balance...
+export LIBEUFIN_SANDBOX_USERNAME=admin
+export LIBEUFIN_SANDBOX_PASSWORD=secret
+./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  new-transaction \
+  --bank-account admin \
+  --payto-with-subject "payto://iban/SANDBOXX/LOCAL?message=bring-to-zero" \
+  --amount EUR:1
+echo DONE
+echo -n Delete the account...
+export LIBEUFIN_SANDBOX_USERNAME=admin
+export LIBEUFIN_SANDBOX_PASSWORD=secret
+./libeufin-cli \
+  sandbox --sandbox-url http://localhost:5000/ \
+  demobank \
+  circuit-delete-account --username www
+echo DONE
diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli
index 0f52f915..d4e0d943 100755
--- a/cli/bin/libeufin-cli
+++ b/cli/bin/libeufin-cli
@@ -14,16 +14,8 @@ from requests import post, get, auth, delete, patch
 from urllib.parse import urljoin
 from getpass import getpass
 
-# Extracts the circuit account username by processing
-# the arguments or the environment.  It gives precedence
-# to the username met on the CLI, defaulting to the environment
-# when that is missing.  Returns the username, or None if that
-# could not be found.  This function helps when a username
-# is a 'resource name'.  Namely, when such username points
-# at a customer username; therefore, the value 'admin' is not
-# accepted since that's never used in that way.
-def get_circuit_username(usernameCli, usernameEnv):
-    maybeUsername = usernameCli
+def get_account_name(accountNameCli, usernameEnv):
+    maybeUsername = accountNameCli
     if not maybeUsername:
         maybeUsername = usernameEnv
     if not maybeUsername:
@@ -42,6 +34,8 @@ def check_response_status(resp, expected_status_code=200):
         print("Response: {}".format(resp.text), file=sys.stderr)
         sys.exit(1)
 
+# Prints unexpected responses without exiting
+# and optionally prints expected respones.
 def tell_user(resp, expected_status_code=200, withsuccess=False):
     if resp.status_code != expected_status_code:
         print(resp.content.decode("utf-8"), file=sys.stderr)
@@ -1535,6 +1529,128 @@ def simulate_incoming_transaction(
 
 # The commands below request to the latest CIRCUIT API.
 
+@sandbox_demobank.command(
+  "circuit-cashout-confirm",
+  help="Confirm a cash-out operation.  Only the author is allowed (no admin)."
+)
+@click.option(
+    "--tan",
+    help="TAN that authorizes the cash-out operaion.",
+    required=True,
+    prompt=True
+)
+@click.option(
+    "--uuid",
+    help="UUID of the cash-out operation to confirm.",
+    required=True,
+    prompt=True
+)
+@click.pass_obj
+def circuit_cashout_confirm(obj, tan, uuid):
+    cashout_confirm_endpoint = obj.circuit_api_url(f"cashouts/{uuid}/confirm")
+    req = dict(tan=tan)
+    try:
+        resp = post(
+            cashout_confirm_endpoint,
+            json=req,
+            auth=auth.HTTPBasicAuth(obj.username, obj.password)
+        )
+    except Exception as e:
+        print(e)
+        print("Could not reach the bank at " + cashout_abort_endpoint)
+        exit(1)
+
+    check_response_status(resp, 204)
+    
+
+@sandbox_demobank.command(
+  "circuit-cashout-abort",
+  help="Abort a cash-out operation.  Admin and author are allowed to request."
+)
+@click.option(
+    "--uuid",
+    help="UUID of the cash-out operation to abort.",
+    required=True,
+    prompt=True
+)
+@click.pass_obj
+def circuit_cashout_abort(obj, uuid):
+    cashout_abort_endpoint = obj.circuit_api_url(f"cashouts/{uuid}/abort")
+    try:
+        resp = post(
+            cashout_abort_endpoint,
+            auth=auth.HTTPBasicAuth(obj.username, obj.password)
+        )
+    except Exception as e:
+        print(e)
+        print("Could not reach the bank at " + cashout_abort_endpoint)
+        exit(1)
+
+    check_response_status(resp, 204)
+
+@sandbox_demobank.command(
+  "circuit-cashout-details",
+  help="Retrieve status information about one cash-out operation.  Admin and 
author are allowed to request."
+)
+@click.option(
+    "--uuid",
+    help="UUID of the cash-out operation to retrieve.",
+    required=True,
+    prompt=True
+)
+@click.pass_obj
+def circuit_cashout_info(obj, uuid):
+    cashout_info_endpoint = obj.circuit_api_url(f"cashouts/{uuid}")
+    try:
+        resp = get(
+            cashout_info_endpoint,
+            auth=auth.HTTPBasicAuth(obj.username, obj.password)
+        )
+    except Exception as e:
+        print(e)
+        print("Could not reach the bank at " + cashout_info_endpoint)
+        exit(1)
+
+    check_response_status(resp)
+    tell_user(resp, withsuccess=True)
+
+@sandbox_demobank.command(
+  "circuit-delete-account",
+  help="Delete one account.  Only available to the administrator and for 
accounts with zero balance."
+)
+@click.option(
+    "--username",
+    help="account to delete",
+    required=True,
+    prompt=True
+)
+@click.pass_obj
+def circuit_delete(obj, username):
+
+    # Check that admin wasn't specified.
+    # Note: even if 'admin' gets through here,
+    # the bank can't delete it because its profile
+    # doesn't get a ordinary entry into the database.
+    if username == "admin":
+        print("Won't delete 'admin'", file=sys.stderr)
+        exit(1)
+
+    # Do not check credentials to let the --no-auth case
+    # function, in case the bank allows it.
+    account_deletion_endpoint = obj.circuit_api_url(f"accounts/{username}")
+    try:
+        resp = delete(
+            account_deletion_endpoint,
+            auth=auth.HTTPBasicAuth(obj.username, obj.password)
+        )
+    except Exception as e:
+        print(e)
+        print("Could not reach sandbox at " + account_deletion_endpoint)
+        exit(1)
+  
+    check_response_status(resp, expected_status_code=204)
+
+
 @sandbox_demobank.command(
   "circuit-register",
   help="Register a new account with cash-out capabilities.  It needs 
administrator credentials, and the new account password exported in 
LIBEUFIN_NEW_CIRCUIT_ACCOUNT_PASSWORD."
@@ -1565,14 +1681,19 @@ def simulate_incoming_transaction(
     "--email",
     help="E-mail address where to send the cash-out TAN.",
 )
+@click.option(
+    "--internal-iban",
+    help="Which IBAN to associate to this account.  The IBAN participates only 
in the local currency circuit.  If missing, the bank generates one.",
+)
 @click.pass_obj
 def circuit_register(
-  obj,
-  username,
-  cashout_address,
-  name,
-  phone,
-  email
+    obj,
+    username,
+    cashout_address,
+    name,
+    phone,
+    email,
+    internal_iban
 ):
     # Check admin is requesting.
     if (obj.username != "admin"):
@@ -1588,8 +1709,6 @@ def circuit_register(
     # Get the bank base URL.
     registration_endpoint = obj.circuit_api_url("accounts")
 
-    # Craft the request.
-
     contact_data = dict()
     if (phone):
         contact_data.update(phone=phone)
@@ -1602,6 +1721,8 @@ def circuit_register(
         name=name,
         cashout_address=cashout_address
     )
+    if internal_iban:
+        req.update(internal_iban=internal_iban)
     try:
         resp = post(
             registration_endpoint,
@@ -1646,7 +1767,7 @@ def circuit_reconfig(
   cashout_address,
   username
 ):
-    resource_name = get_circuit_username(username, obj.username)
+    resource_name = get_account_name(username, obj.username)
     if not resource_name:
         print("Could not find any username to reconfigure.", file=sys.stderr)
     reconfig_endpoint = obj.circuit_api_url(f"accounts/{resource_name}")
@@ -1682,7 +1803,7 @@ def circuit_reconfig(
 )
 @click.pass_obj
 def password_reconfig(obj, username):
-    resource_name = get_circuit_username(username, obj.username)
+    resource_name = get_account_name(username, obj.username)
     if not resource_name:
         print(
             "Couldn't find the username whose password should change.",
@@ -1718,4 +1839,59 @@ def password_reconfig(obj, username):
 
     check_response_status(resp, expected_status_code=204)
 
+
+@sandbox_demobank.command(
+  "circuit-cashout",
+  help="Create a cash-out operation.  If successful, the user gets a TAN."
+)
+@click.option(
+    "--subject",
+    help="Payment subject to associate to the outgoing and incoming payments 
that are associated with this cash-out operation.",
+    required=False
+)
+@click.option(
+    "--amount-debit",
+    help="Amount that will debited to the local currency account, in the 
<currency>:X.Y format.",
+    required=True,
+    prompt=True
+)
+@click.option(
+    "--amount-credit",
+    help="Amount that will credited to the fiat currency account, in the 
<currency>:X.Y format.",
+    required=True,
+    prompt=True
+)
+@click.option(
+    "--tan-channel",
+    help="Indicates how to send the TAN to the user: only 'sms' or 'email' are 
valid values.  If missing, the bank defaults to SMS",
+    required=False
+)
+@click.pass_obj
+def circuit_cashout(obj, subject, amount_debit, amount_credit, tan_channel):
+    # (not) resorting auth credentials, if they're None, request fails at the 
server.
+    # Craft the request.  
+    req = dict(
+        amount_debit=amount_debit,
+        amount_credit=amount_credit
+    )
+    if subject:
+        req.update(subject=subject)
+    if tan_channel:
+        req.update(tan_channel=tan_channel)
+  
+    cashout_creation_endpoint = obj.circuit_api_url("cashouts")
+    try:
+        resp = post(
+            cashout_creation_endpoint,
+            json=req,
+            auth=auth.HTTPBasicAuth(obj.username, obj.password)
+        )
+    except Exception as e:
+        print(e)
+        print("Could not reach sandbox at " + cashout_creation_endpoint)
+        exit(1)
+  
+    check_response_status(resp, expected_status_code=202)
+    tell_user(resp, 202, withsuccess=True) # Communicates back the operation 
UUID.
+
 cli()
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
index e28770f7..e02bdfb3 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
@@ -129,12 +129,19 @@ fun isTanChannelSupported(tanMethod: String): Boolean {
 fun circuitApi(circuitRoute: Route) {
     // Abort a cash-out operation.
     circuitRoute.post("/cashouts/{uuid}/abort") {
-        val user = call.request.basicAuth()
-        val uuid = call.getUriComponent("uuid")
+        call.request.basicAuth() // both admin and author allowed
+        val arg = call.getUriComponent("uuid")
+        // Parse and check the UUID.
+        val maybeUuid = try {
+            UUID.fromString(arg)
+        } catch (e: Exception) {
+            val msg = "The cash-out UUID is invalid: $arg"
+            logger.debug(e.message)
+            logger.debug(msg)
+            throw badRequest(msg)
+        }
         val maybeOperation = transaction {
-            CashoutOperationEntity.find {
-                CashoutOperationsTable.uuid eq UUID.fromString(uuid)
-            }.firstOrNull()
+            CashoutOperationEntity.find { uuid eq maybeUuid }.firstOrNull()
         }
         if (maybeOperation == null) {
             val msg = "Cash-out operation $uuid not found."
@@ -192,6 +199,8 @@ fun circuitApi(circuitRoute: Route) {
          */
         val req = call.receive<CashoutConfirmation>()
         val maybeTanFromEnv = System.getenv("LIBEUFIN_CASHOUT_TEST_TAN")
+        if (maybeTanFromEnv != null)
+            logger.warn("TAN being read from the environment.  Assuming tests 
are being run")
         val checkTan = maybeTanFromEnv ?: op.tan
         if (req.tan != checkTan) {
             logger.debug("The confirmation of '${op.uuid}' has a wrong TAN 
'${req.tan}'")
@@ -216,13 +225,20 @@ fun circuitApi(circuitRoute: Route) {
     }
     // Retrieve the status of a cash-out operation.
     circuitRoute.get("/cashouts/{uuid}") {
-        val user = call.request.basicAuth()
+        call.request.basicAuth() // both admin and author
         val operationUuid = call.getUriComponent("uuid")
+        // Parse and check the UUID.
+        val maybeUuid = try {
+            UUID.fromString(operationUuid)
+        } catch (e: Exception) {
+            val msg = "The cash-out UUID is invalid: $operationUuid"
+            logger.debug(e.message)
+            logger.debug(msg)
+            throw badRequest(msg)
+        }
         // Get the operation from the database.
         val maybeOperation = transaction {
-            CashoutOperationEntity.find {
-                uuid eq UUID.fromString(operationUuid)
-            }.firstOrNull()
+            CashoutOperationEntity.find { uuid eq maybeUuid }.firstOrNull()
         }
         if (maybeOperation == null) {
             val msg = "Cash-out operation $operationUuid not found."
@@ -481,7 +497,8 @@ fun circuitApi(circuitRoute: Route) {
             val newAccount = insertNewAccount(
                 username = req.username,
                 password = req.password,
-                name = req.name
+                name = req.name,
+                iban = req.internal_iban
             )
             newAccount.customer.phone = req.contact_data.phone
             newAccount.customer.email = req.contact_data.email
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index 2b39819e..d1f88b43 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -20,18 +20,12 @@
 package tech.libeufin.sandbox
 
 import UtilError
-import com.fasterxml.jackson.core.JsonParseException
-import com.fasterxml.jackson.core.JsonProcessingException
 import com.fasterxml.jackson.core.util.DefaultIndenter
 import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
-import com.fasterxml.jackson.databind.DeserializationFeature
-import com.fasterxml.jackson.databind.JsonMappingException
 import com.fasterxml.jackson.databind.ObjectMapper
 import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.databind.exc.MismatchedInputException
 import com.fasterxml.jackson.module.kotlin.KotlinFeature
 import com.fasterxml.jackson.module.kotlin.KotlinModule
-import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
 import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.core.context
 import com.github.ajalt.clikt.core.subcommands
@@ -42,7 +36,6 @@ import com.github.ajalt.clikt.parameters.types.int
 import execThrowableOrTerminate
 import io.ktor.server.application.*
 import io.ktor.http.*
-import io.ktor.serialization.*
 import io.ktor.serialization.jackson.*
 import io.ktor.server.engine.*
 import io.ktor.server.netty.*
@@ -55,9 +48,7 @@ import io.ktor.server.routing.*
 import io.ktor.server.util.*
 import io.ktor.server.plugins.callloging.*
 import io.ktor.server.plugins.cors.routing.*
-import io.ktor.util.*
 import io.ktor.util.date.*
-import kotlinx.coroutines.*
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
 import org.jetbrains.exposed.sql.statements.api.ExposedBlob
@@ -1173,13 +1164,19 @@ val sandboxApp: Application.() -> Unit = {
                     return@get
                 }
                 post("/withdrawal-operation/{wopid}") {
-                    val wopid: String = ensureNonNull(call.parameters["wopid"])
+                    val arg = ensureNonNull(call.parameters["wopid"])
+                    val maybeWithdrawalUUid = try {
+                        java.util.UUID.fromString(arg)
+                    } catch (e: Exception) {
+                        logger.debug(e.message)
+                        throw badRequest("Withdrawal operation UUID was 
invalid: $arg")
+                    }
                     val body = call.receive<TalerWithdrawalSelection>()
                     val transferDone = transaction {
                         val wo = TalerWithdrawalEntity.find {
-                            TalerWithdrawalsTable.wopid eq 
java.util.UUID.fromString(wopid)
+                            TalerWithdrawalsTable.wopid eq maybeWithdrawalUUid
                         }.firstOrNull() ?: throw SandboxError(
-                            HttpStatusCode.NotFound, "Withdrawal operation 
$wopid not found."
+                            HttpStatusCode.NotFound, "Withdrawal operation 
$maybeWithdrawalUUid not found."
                         )
                         if (wo.confirmationDone) {
                             return@transaction true
@@ -1211,24 +1208,30 @@ val sandboxApp: Application.() -> Unit = {
                     return@post
                 }
                 get("/withdrawal-operation/{wopid}") {
-                    val wopid: String = ensureNonNull(call.parameters["wopid"])
-                    val wo = transaction {
+                    val arg = ensureNonNull(call.parameters["wopid"])
+                    val maybeWithdrawalUuid = try {
+                        java.util.UUID.fromString(arg)
+                    } catch (e: Exception) {
+                        logger.debug(e.message)
+                        throw badRequest("Withdrawal UUID invalid: $arg")
+                    }
+                    val maybeWithdrawalOp = transaction {
                         TalerWithdrawalEntity.find {
-                            TalerWithdrawalsTable.wopid eq 
java.util.UUID.fromString(wopid)
+                            TalerWithdrawalsTable.wopid eq maybeWithdrawalUuid
                         }.firstOrNull() ?: throw SandboxError(
                             HttpStatusCode.NotFound,
-                            "Withdrawal operation: $wopid not found"
+                            "Withdrawal operation: $arg not found"
                         )
                     }
                     val demobank = ensureDemobank(call)
                     var captcha_page = demobank.captchaUrl
                     if (captcha_page == null) logger.warn("CAPTCHA URL not 
found")
                     val ret = TalerWithdrawalStatus(
-                        selection_done = wo.selectionDone,
-                        transfer_done = wo.confirmationDone,
-                        amount = wo.amount,
+                        selection_done = maybeWithdrawalOp.selectionDone,
+                        transfer_done = maybeWithdrawalOp.confirmationDone,
+                        amount = maybeWithdrawalOp.amount,
                         suggested_exchange = demobank.suggestedExchangeBaseUrl,
-                        aborted = wo.aborted,
+                        aborted = maybeWithdrawalOp.aborted,
                         confirm_transfer_url = captcha_page
                     )
                     call.respond(ret)

-- 
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]