gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (365df3c3 -> 4f7ad130)


From: gnunet
Subject: [libeufin] branch master updated (365df3c3 -> 4f7ad130)
Date: Mon, 18 Sep 2023 14:27:13 +0200

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

ms pushed a change to branch master
in repository libeufin.

    from 365df3c3 Implementing token authentication.
     new 71d6f7a6 renaming file
     new 0a3956ee Bank API.
     new 6f774b6f Testing GET /accounts/{USERNAME}
     new 14f5c806 Testing 401 on GET /accounts/{USERNAME}
     new 4f7ad130 Testing:

The 5 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:
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 100 +++++++++++-
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    | 172 ++-------------------
 .../tech/libeufin/bank/accountsMgmtHandlers.kt     | 141 +++++++++++++++++
 bank/src/main/kotlin/tech/libeufin/bank/helpers.kt |  42 ++++-
 .../kotlin/tech/libeufin/bank/tokenHandlers.kt     |  91 +++++++++++
 .../tech/libeufin/bank/transactionsHandlers.kt     |  97 ++++++++++++
 .../tech/libeufin/bank/{bankTypes.kt => types.kt}  |  54 ++++++-
 bank/src/test/kotlin/Common.kt                     |   1 +
 bank/src/test/kotlin/LibeuFinApiTest.kt            | 121 ++++++++++++++-
 util/src/main/kotlin/HTTP.kt                       |   2 +-
 10 files changed, 641 insertions(+), 180 deletions(-)
 create mode 100644 
bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
 create mode 100644 bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
 create mode 100644 
bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
 rename bank/src/main/kotlin/tech/libeufin/bank/{bankTypes.kt => types.kt} (86%)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 73194c36..be196676 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -177,6 +177,7 @@ class Database(private val dbConfig: String) {
             )
         }
     }
+
     fun customerGetFromLogin(login: String): Customer? {
         reconnect()
         val stmt = prepare("""
@@ -266,6 +267,10 @@ class Database(private val dbConfig: String) {
     // Returns false on conflicts.
     fun bankAccountCreate(bankAccount: BankAccount): Boolean {
         reconnect()
+        if (bankAccount.balance != null)
+            throw internalServerError(
+                "Do not pass a balance upon bank account creation, do a wire 
transfer instead."
+            )
         // FIXME: likely to be changed to only do internal_payto_uri
         val stmt = prepare("""
             INSERT INTO bank_accounts
@@ -317,6 +322,7 @@ class Database(private val dbConfig: String) {
              ,has_debt
              ,(max_debt).val AS max_debt_val
              ,(max_debt).frac AS max_debt_frac
+             ,bank_account_id
             FROM bank_accounts
             WHERE owning_customer_id=?
         """)
@@ -338,7 +344,48 @@ class Database(private val dbConfig: String) {
                 maxDebt = TalerAmount(
                     value = it.getLong("max_debt_val"),
                     frac = it.getInt("max_debt_frac")
-                )
+                ),
+                bankAccountId = it.getLong("bank_account_id")
+            )
+        }
+    }
+    fun bankAccountGetFromInternalPayto(internalPayto: String): BankAccount? {
+        reconnect()
+        val stmt = prepare("""
+            SELECT
+             bank_account_id
+             ,owning_customer_id
+             ,is_public
+             ,is_taler_exchange
+             ,last_nexus_fetch_row_id
+             ,(balance).val AS balance_val
+             ,(balance).frac AS balance_frac
+             ,has_debt
+             ,(max_debt).val AS max_debt_val
+             ,(max_debt).frac AS max_debt_frac
+            FROM bank_accounts
+            WHERE internal_payto_uri=?
+        """)
+        stmt.setString(1, internalPayto)
+
+        val rs = stmt.executeQuery()
+        rs.use {
+            if (!it.next()) return null
+            return BankAccount(
+                internalPaytoUri = internalPayto,
+                balance = TalerAmount(
+                    it.getLong("balance_val"),
+                    it.getInt("balance_frac")
+                ),
+                lastNexusFetchRowId = it.getLong("last_nexus_fetch_row_id"),
+                owningCustomerId = it.getLong("owning_customer_id"),
+                hasDebt = it.getBoolean("has_debt"),
+                isTalerExchange = it.getBoolean("is_taler_exchange"),
+                maxDebt = TalerAmount(
+                    value = it.getLong("max_debt_val"),
+                    frac = it.getInt("max_debt_frac")
+                ),
+                bankAccountId = it.getLong("bank_account_id")
             )
         }
     }
@@ -388,6 +435,57 @@ class Database(private val dbConfig: String) {
         }
     }
 
+    // Get the bank transaction whose row ID is rowId
+    fun bankTransactionGetFromInternalId(rowId: Long): BankAccountTransaction? 
{
+        reconnect()
+        val stmt = prepare("""
+            SELECT 
+              creditor_payto_uri
+              ,creditor_name
+              ,debtor_payto_uri
+              ,debtor_name
+              ,subject
+              ,(amount).val AS amount_val
+              ,(amount).frac AS amount_frac
+              ,transaction_date
+              ,account_servicer_reference
+              ,payment_information_id
+              ,end_to_end_id
+              ,direction
+              ,bank_account_id
+            FROM bank_account_transactions
+               WHERE bank_transaction_id=?
+        """)
+        stmt.setLong(1, rowId)
+        val rs = stmt.executeQuery()
+        rs.use {
+            if (!it.next()) return null
+            return BankAccountTransaction(
+                creditorPaytoUri = it.getString("creditor_payto_uri"),
+                creditorName = it.getString("creditor_name"),
+                debtorPaytoUri = it.getString("debtor_payto_uri"),
+                debtorName = it.getString("debtor_name"),
+                amount = TalerAmount(
+                    it.getLong("amount_val"),
+                    it.getInt("amount_frac")
+                ),
+                accountServicerReference = 
it.getString("account_servicer_reference"),
+                endToEndId = it.getString("end_to_end_id"),
+                direction = it.getString("direction").run {
+                    when(this) {
+                        "credit" -> TransactionDirection.credit
+                        "debit" -> TransactionDirection.debit
+                        else -> throw internalServerError("Wrong direction in 
transaction: $this")
+                    }
+                },
+                bankAccountId = it.getLong("bank_account_id"),
+                paymentInformationId = it.getString("payment_information_id"),
+                subject = it.getString("subject"),
+                transactionDate = it.getLong("transaction_date")
+            )
+        }
+    }
+
     fun bankTransactionGetForHistoryPage(
         upperBound: Long,
         bankAccountId: Long,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 2e30038d..fe4e4814 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -39,13 +39,11 @@ import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.json.*
 import kotlinx.serialization.modules.SerializersModule
 import net.taler.common.errorcodes.TalerErrorCode
-import net.taler.wallet.crypto.Base32Crockford
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import org.slf4j.event.Level
 import tech.libeufin.util.*
 import java.time.Duration
-import kotlin.random.Random
 
 // GLOBALS
 val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank")
@@ -196,6 +194,7 @@ val webApp: Application.() -> Unit = {
         }
         // Catch-all branch to mean that the bank wasn't able to manage one 
error.
         exception<Exception> {call, cause ->
+            cause.printStackTrace()
             logger.error(cause.message)
             call.respond(
                 status = HttpStatusCode.InternalServerError,
@@ -207,167 +206,14 @@ val webApp: Application.() -> Unit = {
         }
     }
     routing {
-        post("/accounts/{USERNAME}/token") {
-            val customer = call.myAuth(TokenScope.refreshable) ?: throw 
unauthorized("Authentication failed")
-            val endpointOwner = call.expectUriComponent("USERNAME")
-            if (customer.login != endpointOwner)
-                throw forbidden(
-                    "User has no rights on this enpoint",
-                    TalerErrorCode.TALER_EC_END // FIXME: need generic 
forbidden
-                )
-            val maybeAuthToken = call.getAuthToken()
-            val req = call.receive<TokenRequest>()
-            /**
-             * This block checks permissions ONLY IF the call was authenticated
-             * with a token.  Basic auth gets always granted.
-             */
-            if (maybeAuthToken != null) {
-                val tokenBytes = Base32Crockford.decode(maybeAuthToken)
-                val refreshingToken = db.bearerTokenGet(tokenBytes) ?: throw 
internalServerError(
-                    "Token used to auth not found in the database!"
-                )
-                if (refreshingToken.scope == TokenScope.readonly && req.scope 
== TokenScope.readwrite)
-                    throw forbidden(
-                        "Cannot generate RW token from RO",
-                        
TalerErrorCode.TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT
-                    )
-            }
-            val tokenBytes = ByteArray(32).apply {
-                java.util.Random().nextBytes(this)
-            }
-            val maxDurationTime: Long = db.configGet("token_max_duration").run 
{
-                if (this == null)
-                    return@run Long.MAX_VALUE
-                return@run try {
-                    this.toLong()
-                } catch (e: Exception) {
-                    logger.error("Could not convert config's 
token_max_duration to Long")
-                    throw internalServerError(e.message)
-                }
-            }
-            if (req.duration != null && 
req.duration.d_us.compareTo(maxDurationTime) == 1)
-                throw forbidden(
-                    "Token duration bigger than bank's limit",
-                    // FIXME: define new EC for this case.
-                    TalerErrorCode.TALER_EC_END
-                )
-            val tokenDurationUs  = req.duration?.d_us ?: 
TOKEN_DEFAULT_DURATION_US
-            val customerDbRow = customer.dbRowId ?: throw internalServerError(
-                "Coud not resort customer '${customer.login}' database row ID"
-            )
-            val expirationTimestampUs: Long = getNowUs() + tokenDurationUs
-            if (expirationTimestampUs < tokenDurationUs)
-                throw badRequest(
-                    "Token duration caused arithmetic overflow",
-                    // FIXME: need dedicate EC (?)
-                    talerErrorCode = TalerErrorCode.TALER_EC_END
-                )
-            val token = BearerToken(
-                bankCustomer = customerDbRow,
-                content = tokenBytes,
-                creationTime = expirationTimestampUs,
-                expirationTime = expirationTimestampUs,
-                scope = req.scope,
-                isRefreshable = req.refreshable
-            )
-            if (!db.bearerTokenCreate(token))
-                throw internalServerError("Failed at inserting new token in 
the database")
-            call.respond(TokenSuccessResponse(
-                access_token = Base32Crockford.encode(tokenBytes),
-                expiration = Timestamp(
-                    t_s = expirationTimestampUs / 1000000L
-                )
-            ))
-            return@post
-        }
-        post("/accounts") {
-            // check if only admin.
-            val maybeOnlyAdmin = db.configGet("only_admin_registrations")
-            if (maybeOnlyAdmin?.lowercase() == "yes") {
-                val customer: Customer? = call.myAuth(TokenScope.readwrite)
-                if (customer == null || customer.login != "admin")
-                    throw LibeufinBankException(
-                        httpStatus = HttpStatusCode.Unauthorized,
-                        talerError = TalerError(
-                            code = 
TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code,
-                            hint = "Either 'admin' not authenticated or an 
ordinary user tried this operation."
-                        )
-                    )
-            }
-            // auth passed, proceed with activity.
-            val req = call.receive<RegisterAccountRequest>()
-            // Prohibit reserved usernames:
-            if (req.username == "admin" || req.username == "bank")
-                throw LibeufinBankException(
-                    httpStatus = HttpStatusCode.Conflict,
-                    talerError = TalerError(
-                        code = GENERIC_UNDEFINED, // FIXME: this waits GANA.
-                        hint = "Username '${req.username}' is reserved."
-                    )
-                )
-            // Checking imdepotency.
-            val maybeCustomerExists = db.customerGetFromLogin(req.username)
-            // Can be null if previous call crashed before completion.
-            val maybeHasBankAccount = maybeCustomerExists.run {
-                if (this == null) return@run null
-                db.bankAccountGetFromOwnerId(this.expectRowId())
-            }
-            if (maybeCustomerExists != null && maybeHasBankAccount != null) {
-                logger.debug("Registering username was found: 
${maybeCustomerExists.login}")
-                // Checking _all_ the details are the same.
-                val isIdentic =
-                    maybeCustomerExists.name == req.name &&
-                    maybeCustomerExists.email == 
req.challenge_contact_data?.email &&
-                    maybeCustomerExists.phone == 
req.challenge_contact_data?.phone &&
-                    maybeCustomerExists.cashoutPayto == req.cashout_payto_uri 
&&
-                    CryptoUtil.checkpw(req.password, 
maybeCustomerExists.passwordHash) &&
-                    maybeHasBankAccount.isPublic == req.is_public &&
-                    maybeHasBankAccount.isTalerExchange == 
req.is_taler_exchange &&
-                    maybeHasBankAccount.internalPaytoUri == 
req.internal_payto_uri
-                if (isIdentic) {
-                    call.respond(HttpStatusCode.Created)
-                    return@post
-                }
-                throw LibeufinBankException(
-                    httpStatus = HttpStatusCode.Conflict,
-                    talerError = TalerError(
-                        code = GENERIC_UNDEFINED, // GANA needs this.
-                        hint = "Idempotency check failed."
-                    )
-                )
-            }
-            // From here: fresh user being added.
-            val newCustomer = Customer(
-                login = req.username,
-                name = req.name,
-                email = req.challenge_contact_data?.email,
-                phone = req.challenge_contact_data?.phone,
-                cashoutPayto = req.cashout_payto_uri,
-                // Following could be gone, if included in cashout_payto_uri
-                cashoutCurrency = db.configGet("cashout_currency"),
-                passwordHash = CryptoUtil.hashpw(req.password)
-            )
-            val newCustomerRowId = db.customerCreate(newCustomer)
-                ?: throw internalServerError("New customer INSERT failed 
despite the previous checks")
-            /* Crashing here won't break data consistency between customers
-             * and bank accounts, because of the idempotency.  Client will
-             * just have to retry.  */
-            val maxDebt = db.configGet("max_debt_ordinary_customers").run {
-                if (this == null) throw internalServerError("Max debt not 
configured")
-                parseTalerAmount(this)
-            }
-            val newBankAccount = BankAccount(
-                hasDebt = false,
-                internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
-                owningCustomerId = newCustomerRowId,
-                isPublic = req.is_public,
-                isTalerExchange = req.is_taler_exchange,
-                maxDebt = maxDebt
-            )
-            if (!db.bankAccountCreate(newBankAccount))
-                throw internalServerError("Could not INSERT bank account 
despite all the checks.")
-            call.respond(HttpStatusCode.Created)
-            return@post
+        get("/config") {
+            call.respond(Config())
+            return@get
         }
+        this.accountsMgmtHandlers()
+        this.tokenHandlers()
+        this.transactionsHandlers()
+        // this.talerHandlers()
+        // this.walletIntegrationHandlers()
     }
 }
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
new file mode 100644
index 00000000..1608aa19
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
@@ -0,0 +1,141 @@
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import net.taler.common.errorcodes.TalerErrorCode
+import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.maybeUriComponent
+
+/**
+ * This function collects all the /accounts handlers that
+ * create, update, delete, show bank accounts.  No histories
+ * and wire transfers should belong here.
+ */
+fun Routing.accountsMgmtHandlers() {
+    post("/accounts") {
+        // check if only admin.
+        val maybeOnlyAdmin = db.configGet("only_admin_registrations")
+        if (maybeOnlyAdmin?.lowercase() == "yes") {
+            val customer: Customer? = call.myAuth(TokenScope.readwrite)
+            if (customer == null || customer.login != "admin")
+                throw LibeufinBankException(
+                    httpStatus = HttpStatusCode.Unauthorized,
+                    talerError = TalerError(
+                        code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code,
+                        hint = "Either 'admin' not authenticated or an 
ordinary user tried this operation."
+                    )
+                )
+        }
+        // auth passed, proceed with activity.
+        val req = call.receive<RegisterAccountRequest>()
+        // Prohibit reserved usernames:
+        if (req.username == "admin" || req.username == "bank")
+            throw LibeufinBankException(
+                httpStatus = HttpStatusCode.Conflict,
+                talerError = TalerError(
+                    code = GENERIC_UNDEFINED, // FIXME: this waits GANA.
+                    hint = "Username '${req.username}' is reserved."
+                )
+            )
+        // Checking imdepotency.
+        val maybeCustomerExists = db.customerGetFromLogin(req.username)
+        // Can be null if previous call crashed before completion.
+        val maybeHasBankAccount = maybeCustomerExists.run {
+            if (this == null) return@run null
+            db.bankAccountGetFromOwnerId(this.expectRowId())
+        }
+        if (maybeCustomerExists != null && maybeHasBankAccount != null) {
+            tech.libeufin.bank.logger.debug("Registering username was found: 
${maybeCustomerExists.login}")
+            // Checking _all_ the details are the same.
+            val isIdentic =
+                maybeCustomerExists.name == req.name &&
+                        maybeCustomerExists.email == 
req.challenge_contact_data?.email &&
+                        maybeCustomerExists.phone == 
req.challenge_contact_data?.phone &&
+                        maybeCustomerExists.cashoutPayto == 
req.cashout_payto_uri &&
+                        CryptoUtil.checkpw(req.password, 
maybeCustomerExists.passwordHash) &&
+                        maybeHasBankAccount.isPublic == req.is_public &&
+                        maybeHasBankAccount.isTalerExchange == 
req.is_taler_exchange &&
+                        maybeHasBankAccount.internalPaytoUri == 
req.internal_payto_uri
+            if (isIdentic) {
+                call.respond(HttpStatusCode.Created)
+                return@post
+            }
+            throw LibeufinBankException(
+                httpStatus = HttpStatusCode.Conflict,
+                talerError = TalerError(
+                    code = GENERIC_UNDEFINED, // GANA needs this.
+                    hint = "Idempotency check failed."
+                )
+            )
+        }
+        // From here: fresh user being added.
+        val newCustomer = Customer(
+            login = req.username,
+            name = req.name,
+            email = req.challenge_contact_data?.email,
+            phone = req.challenge_contact_data?.phone,
+            cashoutPayto = req.cashout_payto_uri,
+            // Following could be gone, if included in cashout_payto_uri
+            cashoutCurrency = db.configGet("cashout_currency"),
+            passwordHash = CryptoUtil.hashpw(req.password),
+        )
+        val newCustomerRowId = db.customerCreate(newCustomer)
+            ?: throw internalServerError("New customer INSERT failed despite 
the previous checks")
+        /* Crashing here won't break data consistency between customers
+         * and bank accounts, because of the idempotency.  Client will
+         * just have to retry.  */
+        val maxDebt = db.configGet("max_debt_ordinary_customers").run {
+            if (this == null) throw internalServerError("Max debt not 
configured")
+            parseTalerAmount(this)
+        }
+        val bonus = db.configGet("registration_bonus")
+        val initialBalance = if (bonus != null) parseTalerAmount(bonus) else 
TalerAmount(0, 0)
+        val newBankAccount = BankAccount(
+            hasDebt = false,
+            internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
+            owningCustomerId = newCustomerRowId,
+            isPublic = req.is_public,
+            isTalerExchange = req.is_taler_exchange,
+            maxDebt = maxDebt,
+            balance = initialBalance
+        )
+        if (!db.bankAccountCreate(newBankAccount))
+            throw internalServerError("Could not INSERT bank account despite 
all the checks.")
+        call.respond(HttpStatusCode.Created)
+        return@post
+    }
+    get("/accounts/{USERNAME}") {
+        val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized("Login 
failed")
+        val resourceName = call.maybeUriComponent("USERNAME") ?: throw 
badRequest(
+            hint = "No username found in the URI",
+            talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
+        )
+        // Checking resource name only if Basic auth was used.
+        // Successful tokens do not need this check, they just pass.
+        if (
+            ((c.login != resourceName)
+            && (c.login != "admin"))
+            && (call.getAuthToken() == null)
+            )
+            throw forbidden("No rights on the resource.")
+        val customerData = db.customerGetFromLogin(c.login) ?: throw 
internalServerError("Customer '${c.login} despite being authenticated.'")
+        val customerInternalId = customerData.dbRowId ?: throw 
internalServerError("Customer '${c.login} had no row ID despite it was found in 
the database.'")
+        val bankAccountData = db.bankAccountGetFromOwnerId(customerInternalId) 
?: throw internalServerError("Customer '${c.login} had no bank account despite 
they are customer.'")
+        call.respond(AccountData(
+            name = customerData.name,
+            balance = bankAccountData.balance.toString(),
+            debit_threshold = bankAccountData.maxDebt.toString(),
+            payto_uri = bankAccountData.internalPaytoUri,
+            contact_data = ChallengeContactData(
+                email = customerData.email,
+                phone = customerData.phone
+            ),
+            cashout_payto_uri = customerData.cashoutPayto,
+            has_debit = bankAccountData.hasDebt
+        ))
+        return@get
+    }
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 28c58fe5..258c1f96 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -26,6 +26,11 @@ import net.taler.wallet.crypto.Base32Crockford
 import tech.libeufin.util.*
 import java.lang.NumberFormatException
 
+fun ApplicationCall.expectUriComponent(componentName: String) =
+    this.maybeUriComponent(componentName) ?: throw badRequest(
+        hint = "No username found in the URI",
+        talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
+)
 // Get the auth token (stripped of the bearer-token:-prefix)
 // IF the call was authenticated with it.
 fun ApplicationCall.getAuthToken(): String? {
@@ -131,7 +136,11 @@ fun doTokenAuth(
             ))
 }
 
-fun forbidden(hint: String? = null, talerErrorCode: TalerErrorCode): 
LibeufinBankException =
+fun forbidden(
+    hint: String = "No rights on the resource",
+    // FIXME: create a 'generic forbidden' Taler EC.
+    talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_END
+): LibeufinBankException =
     LibeufinBankException(
         httpStatus = HttpStatusCode.Forbidden,
         talerError = TalerError(
@@ -140,7 +149,7 @@ fun forbidden(hint: String? = null, talerErrorCode: 
TalerErrorCode): LibeufinBan
         )
     )
 
-fun unauthorized(hint: String? = null): LibeufinBankException =
+fun unauthorized(hint: String = "Login failed"): LibeufinBankException =
     LibeufinBankException(
         httpStatus = HttpStatusCode.Unauthorized,
         talerError = TalerError(
@@ -156,6 +165,31 @@ fun internalServerError(hint: String?): 
LibeufinBankException =
             hint = hint
         )
     )
+
+
+fun notFound(
+    hint: String?,
+    talerEc: TalerErrorCode
+): LibeufinBankException =
+    LibeufinBankException(
+        httpStatus = HttpStatusCode.NotFound,
+        talerError = TalerError(
+            code = talerEc.code,
+            hint = hint
+        )
+    )
+
+fun conflict(
+    hint: String?,
+    talerEc: TalerErrorCode
+): LibeufinBankException =
+    LibeufinBankException(
+        httpStatus = HttpStatusCode.Conflict,
+        talerError = TalerError(
+            code = talerEc.code,
+            hint = hint
+        )
+    )
 fun badRequest(
     hint: String? = null,
     talerErrorCode: TalerErrorCode = 
TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
@@ -221,4 +255,6 @@ fun parseTalerAmount(
         frac = fraction,
         maybeCurrency = match.destructured.component1()
     )
-}
\ No newline at end of file
+}
+
+fun getBankCurrency(): String = db.configGet("internal_currency") ?: throw 
internalServerError("Bank lacks currency")
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
new file mode 100644
index 00000000..0b665069
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
@@ -0,0 +1,91 @@
+package tech.libeufin.bank
+
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import net.taler.common.errorcodes.TalerErrorCode
+import net.taler.wallet.crypto.Base32Crockford
+import tech.libeufin.util.maybeUriComponent
+import tech.libeufin.util.getNowUs
+
+fun Routing.tokenHandlers() {
+    delete("/accounts/{USERNAME}/token") {
+        throw internalServerError("Token deletion not implemented.")
+    }
+    post("/accounts/{USERNAME}/token") {
+        val customer = call.myAuth(TokenScope.refreshable) ?: throw 
unauthorized("Authentication failed")
+        val endpointOwner = call.maybeUriComponent("USERNAME")
+        if (customer.login != endpointOwner)
+            throw forbidden(
+                "User has no rights on this enpoint",
+                TalerErrorCode.TALER_EC_END // FIXME: need generic forbidden
+            )
+        val maybeAuthToken = call.getAuthToken()
+        val req = call.receive<TokenRequest>()
+        /**
+         * This block checks permissions ONLY IF the call was authenticated
+         * with a token.  Basic auth gets always granted.
+         */
+        if (maybeAuthToken != null) {
+            val tokenBytes = Base32Crockford.decode(maybeAuthToken)
+            val refreshingToken = db.bearerTokenGet(tokenBytes) ?: throw 
internalServerError(
+                "Token used to auth not found in the database!"
+            )
+            if (refreshingToken.scope == TokenScope.readonly && req.scope == 
TokenScope.readwrite)
+                throw forbidden(
+                    "Cannot generate RW token from RO",
+                    
TalerErrorCode.TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT
+                )
+        }
+        val tokenBytes = ByteArray(32).apply {
+            java.util.Random().nextBytes(this)
+        }
+        val maxDurationTime: Long = db.configGet("token_max_duration").run {
+            if (this == null)
+                return@run Long.MAX_VALUE
+            return@run try {
+                this.toLong()
+            } catch (e: Exception) {
+                tech.libeufin.bank.logger.error("Could not convert config's 
token_max_duration to Long")
+                throw internalServerError(e.message)
+            }
+        }
+        if (req.duration != null && 
req.duration.d_us.compareTo(maxDurationTime) == 1)
+            throw forbidden(
+                "Token duration bigger than bank's limit",
+                // FIXME: define new EC for this case.
+                TalerErrorCode.TALER_EC_END
+            )
+        val tokenDurationUs  = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION_US
+        val customerDbRow = customer.dbRowId ?: throw internalServerError(
+            "Coud not resort customer '${customer.login}' database row ID"
+        )
+        val expirationTimestampUs: Long = getNowUs() + tokenDurationUs
+        if (expirationTimestampUs < tokenDurationUs)
+            throw badRequest(
+                "Token duration caused arithmetic overflow",
+                // FIXME: need dedicate EC (?)
+                talerErrorCode = TalerErrorCode.TALER_EC_END
+            )
+        val token = BearerToken(
+            bankCustomer = customerDbRow,
+            content = tokenBytes,
+            creationTime = expirationTimestampUs,
+            expirationTime = expirationTimestampUs,
+            scope = req.scope,
+            isRefreshable = req.refreshable
+        )
+        if (!db.bearerTokenCreate(token))
+            throw internalServerError("Failed at inserting new token in the 
database")
+        call.respond(
+            TokenSuccessResponse(
+                access_token = Base32Crockford.encode(tokenBytes),
+                expiration = Timestamp(
+                    t_s = expirationTimestampUs / 1000000L
+                )
+            )
+        )
+        return@post
+    }
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
new file mode 100644
index 00000000..0fe3ad69
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
@@ -0,0 +1,97 @@
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import net.taler.common.errorcodes.TalerErrorCode
+import tech.libeufin.util.getNowUs
+import tech.libeufin.util.parsePayto
+
+fun Routing.transactionsHandlers() {
+    // Creates a bank transaction.
+    post("/accounts/{USERNAME}/transactions") {
+        val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+        val resourceName = call.expectUriComponent("USERNAME")
+        // admin has no rights here.
+        if ((c.login != resourceName) && (call.getAuthToken() == null))
+            throw forbidden()
+        val txData = call.receive<BankAccountTransactionCreate>()
+        // FIXME: make payto parser IBAN-agnostic?
+        val payto = parsePayto(txData.payto_uri) ?: throw badRequest("Invalid 
creditor Payto")
+        val paytoWithoutParams = "payto://iban/${payto.bic}/${payto.iban}"
+        val subject = payto.message ?: throw badRequest("Wire transfer lacks 
subject")
+        val debtorId = c.dbRowId ?: throw internalServerError("Debtor database 
ID not found")
+        // This performs already a SELECT on the bank account,
+        // like the wire transfer will do as well later!
+        val creditorCustomerData = 
db.bankAccountGetFromInternalPayto(paytoWithoutParams)
+            ?: throw notFound(
+                "Creditor account not found",
+                TalerErrorCode.TALER_EC_END // FIXME: define this EC.
+            )
+        val amount = parseTalerAmount(txData.amount)
+        if (amount.currency != getBankCurrency())
+            throw badRequest(
+                "Wrong currency: ${amount.currency}",
+                talerErrorCode = 
TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+            )
+        val dbInstructions = BankInternalTransaction(
+            debtorAccountId = debtorId,
+            creditorAccountId = creditorCustomerData.owningCustomerId,
+            subject = subject,
+            amount = amount,
+            transactionDate = getNowUs()
+        )
+        val res = db.bankTransactionCreate(dbInstructions)
+        when(res) {
+            Database.BankTransactionResult.CONFLICT ->
+                throw conflict(
+                    "Insufficient funds",
+                    TalerErrorCode.TALER_EC_END // FIXME: need bank 
'insufficient funds' EC.
+                )
+            Database.BankTransactionResult.NO_CREDITOR ->
+                throw internalServerError("Creditor not found despite previous 
checks.")
+            Database.BankTransactionResult.NO_DEBTOR ->
+                throw internalServerError("Debtor not found despite the 
request was authenticated.")
+            Database.BankTransactionResult.SUCCESS -> 
call.respond(HttpStatusCode.OK)
+        }
+        return@post
+    }
+    get("/accounts/{USERNAME}/transactions/{T_ID}") {
+        val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+        val accountOwner = call.expectUriComponent("USERNAME")
+        // auth ok, check rights.
+        if (c.login != "admin" && c.login != accountOwner)
+            throw forbidden()
+        // rights ok, check tx exists.
+        val tId = call.expectUriComponent("T_ID")
+        val txRowId = try {
+            tId.toLong()
+        } catch (e: Exception) {
+            logger.error(e.message)
+            throw badRequest("TRANSACTION_ID is not a number: ${tId}")
+        }
+        val customerRowId = c.dbRowId ?: throw 
internalServerError("Authenticated client lacks database entry")
+        val tx = db.bankTransactionGetFromInternalId(txRowId)
+            ?: throw notFound(
+                "Bank transaction '$tId' not found",
+                TalerErrorCode.TALER_EC_NONE // FIXME: need def.
+            )
+        val customerBankAccount = db.bankAccountGetFromOwnerId(customerRowId)
+            ?: throw internalServerError("Customer '${c.login}' lacks bank 
account.")
+        if (tx.bankAccountId != customerBankAccount.bankAccountId)
+            throw forbidden("Client has no rights over the bank transaction: 
$tId")
+        // auth and rights, respond.
+        call.respond(BankAccountTransactionInfo(
+            amount = 
"${tx.amount.currency}:${tx.amount.value}.${tx.amount.frac}",
+            creditor_payto_uri = tx.creditorPaytoUri,
+            debtor_payto_uri = tx.debtorPaytoUri,
+            date = tx.transactionDate,
+            direction = tx.direction,
+            subject = tx.subject,
+            row_id = txRowId
+        ))
+        return@get
+    }
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/bankTypes.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
similarity index 86%
rename from bank/src/main/kotlin/tech/libeufin/bank/bankTypes.kt
rename to bank/src/main/kotlin/tech/libeufin/bank/types.kt
index 25e7edca..d0d7d2c6 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/bankTypes.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -172,6 +172,10 @@ class TalerAmount(
                 other.frac == this.frac &&
                 other.currency == this.currency
     }
+
+    override fun toString(): String {
+        return "$currency:$value.$frac"
+    }
 }
 
 /**
@@ -182,6 +186,7 @@ data class BankAccount(
     val internalPaytoUri: String,
     // Database row ID of the customer that owns this bank account.
     val owningCustomerId: Long,
+    val bankAccountId: Long? = null, // null at INSERT.
     val isPublic: Boolean = false,
     val isTalerExchange: Boolean = false,
     /**
@@ -195,7 +200,7 @@ data class BankAccount(
      * being wired by wallet owners.
      */
     val lastNexusFetchRowId: Long = 0L,
-    val balance: TalerAmount? = null,
+    val balance: TalerAmount? = null, // null when a new bank account gets 
created.
     val hasDebt: Boolean,
     val maxDebt: TalerAmount
 )
@@ -254,9 +259,9 @@ data class BankInternalTransaction(
     val subject: String,
     val amount: TalerAmount,
     val transactionDate: Long,
-    val accountServicerReference: String, // ISO20022
-    val endToEndId: String, // ISO20022
-    val paymentInformationId: String // ISO20022
+    val accountServicerReference: String = "not used", // ISO20022
+    val endToEndId: String = "not used", // ISO20022
+    val paymentInformationId: String = "not used" // ISO20022
 )
 
 /**
@@ -326,4 +331,45 @@ data class Cashout(
     val bankAccount: Long,
     val credit_payto_uri: String,
     val cashoutCurrency: String
+)
+
+// Type to return as GET /config response
+@Serializable // Never used to parse JSON.
+data class Config(
+    val name: String = "libeufin-bank",
+    val version: String = "0:0:0",
+    val have_cashout: Boolean = false,
+    // Following might probably get renamed:
+    val fiat_currency: String? = null
+)
+
+// GET /accounts/$USERNAME response.
+@Serializable
+data class AccountData(
+    val name: String,
+    val balance: String,
+    val payto_uri: String,
+    val debit_threshold: String,
+    val contact_data: ChallengeContactData? = null,
+    val cashout_payto_uri: String? = null,
+    val has_debit: Boolean
+)
+
+// Type of POST /transactions
+@Serializable
+data class BankAccountTransactionCreate(
+    val payto_uri: String,
+    val amount: String
+)
+
+// GET /transactions/T_ID
+@Serializable
+data class BankAccountTransactionInfo(
+    val creditor_payto_uri: String,
+    val debtor_payto_uri: String,
+    val amount: String,
+    val direction: TransactionDirection,
+    val subject: String,
+    val row_id: Long, // is T_ID
+    val date: Long
 )
\ No newline at end of file
diff --git a/bank/src/test/kotlin/Common.kt b/bank/src/test/kotlin/Common.kt
index 8aa7e456..52547e88 100644
--- a/bank/src/test/kotlin/Common.kt
+++ b/bank/src/test/kotlin/Common.kt
@@ -20,6 +20,7 @@
 import tech.libeufin.bank.Database
 import tech.libeufin.util.execCommand
 
+// Init the database and sets the currency to KUDOS.
 fun initDb(): Database {
     System.setProperty(
         "BANK_DB_CONNECTION_STRING",
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt 
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 4ee9a803..ab709804 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -1,9 +1,9 @@
-import io.ktor.auth.*
 import io.ktor.client.plugins.*
 import io.ktor.client.request.*
 import io.ktor.client.statement.*
 import io.ktor.http.*
 import io.ktor.server.testing.*
+import kotlinx.serialization.json.Json
 import net.taler.wallet.crypto.Base32Crockford
 import org.junit.Test
 import tech.libeufin.bank.*
@@ -14,14 +14,63 @@ import kotlin.random.Random
 
 class LibeuFinApiTest {
     private val customerFoo = Customer(
-    login = "foo",
-    passwordHash = CryptoUtil.hashpw("pw"),
-    name = "Foo",
-    phone = "+00",
-    email = "foo@b.ar",
-    cashoutPayto = "payto://external-IBAN",
-    cashoutCurrency = "KUDOS"
+        login = "foo",
+        passwordHash = CryptoUtil.hashpw("pw"),
+        name = "Foo",
+        phone = "+00",
+        email = "foo@b.ar",
+        cashoutPayto = "payto://external-IBAN",
+        cashoutCurrency = "KUDOS"
     )
+    private val customerBar = Customer(
+        login = "bar",
+        passwordHash = CryptoUtil.hashpw("pw"),
+        name = "Bar",
+        phone = "+99",
+        email = "bar@example.com",
+        cashoutPayto = "payto://external-IBAN",
+        cashoutCurrency = "KUDOS"
+    )
+    private fun genBankAccount(rowId: Long) = BankAccount(
+        hasDebt = false,
+        internalPaytoUri = "payto://iban/SANDBOXX/${rowId}-IBAN",
+        maxDebt = TalerAmount(100, 0),
+        owningCustomerId = rowId
+    )
+
+    // Testing the creation of bank transactions.
+    @Test
+    fun postTransactionsTest() {
+        val db = initDb()
+        // foo account
+        val fooId = db.customerCreate(customerFoo); assert(fooId != null)
+        assert(db.bankAccountCreate(genBankAccount(fooId!!)))
+        // bar account
+        val barId = db.customerCreate(customerBar); assert(barId != null)
+        assert(db.bankAccountCreate(genBankAccount(barId!!)))
+        // accounts exist, now create one transaction.
+        testApplication {
+            application(webApp)
+            client.post("/accounts/foo/transactions") {
+                expectSuccess = true
+                basicAuth("foo", "pw")
+                contentType(ContentType.Application.Json)
+                // expectSuccess = true
+                setBody("""{
+                    "payto_uri": 
"payto://iban/SANDBOXX/${barId}-IBAN?message=payout", 
+                    "amount": "KUDOS:3.3"
+                }
+                """.trimIndent())
+            }
+            // Getting the only tx that exists in the DB, hence has ID == 1.
+            val r = client.get("/accounts/foo/transactions/1") {
+                basicAuth("foo", "pw")
+                expectSuccess = true
+            }
+            val obj: BankAccountTransactionInfo = 
Json.decodeFromString(r.bodyAsText())
+            assert(obj.subject == "payout")
+        }
+    }
     // Checking the POST /token handling.
     @Test
     fun tokenTest() {
@@ -62,6 +111,62 @@ class LibeuFinApiTest {
             }
         }
     }
+
+    /**
+     * Testing the retrieval of account information.
+     * The tested logic is the one usually needed by SPAs
+     * to show customers their status.
+     */
+    @Test
+    fun getAccountTest() {
+        // Artificially insert a customer and bank account in the database.
+        val db = initDb()
+        val customerRowId = db.customerCreate(Customer(
+            "foo",
+            CryptoUtil.hashpw("pw"),
+            "Foo"
+        ))
+        assert(customerRowId != null)
+        assert(db.bankAccountCreate(
+            BankAccount(
+                hasDebt = false,
+                internalPaytoUri = "payto://iban/SANDBOXX/FOO-IBAN",
+                maxDebt = TalerAmount(100, 0),
+                owningCustomerId = customerRowId!!
+            )
+        ))
+        testApplication {
+            application(webApp)
+            val r = client.get("/accounts/foo") {
+                expectSuccess = true
+                basicAuth("foo", "pw")
+            }
+            val obj: AccountData = Json.decodeFromString(r.bodyAsText())
+            assert(obj.name == "Foo")
+            // Checking admin can.
+            val adminRowId = db.customerCreate(Customer(
+                "admin",
+                CryptoUtil.hashpw("admin"),
+                "Admin"
+            ))
+            assert(adminRowId != null)
+            assert(db.bankAccountCreate(BankAccount(
+                hasDebt = false,
+                internalPaytoUri = "payto://iban/SANDBOXX/ADMIN-IBAN",
+                maxDebt = TalerAmount(100, 0),
+                owningCustomerId = adminRowId!!
+            )))
+            client.get("/accounts/foo") {
+                expectSuccess = true
+                basicAuth("admin", "admin")
+            }
+            val shouldNot = client.get("/accounts/foo") {
+                basicAuth("not", "not")
+                expectSuccess = false
+            }
+            assert(shouldNot.status == HttpStatusCode.Unauthorized)
+        }
+    }
     /**
      * Testing the account creation, its idempotency and
      * the restriction to admin to create accounts.
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index 0f49daa9..9bb769ea 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -50,7 +50,7 @@ fun ApplicationRequest.getBaseUrl(): String? {
  * Get the URI (path's) component or throw Internal server error.
  * @param component the name of the URI component to return.
  */
-fun ApplicationCall.expectUriComponent(name: String): String? {
+fun ApplicationCall.maybeUriComponent(name: String): String? {
     val ret: String? = this.parameters[name]
     if (ret == null) {
         logger.error("Component $name not found in URI")

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