[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated (236b29f9 -> fb40741b)
From: |
gnunet |
Subject: |
[libeufin] branch master updated (236b29f9 -> fb40741b) |
Date: |
Sun, 24 Sep 2023 14:18:54 +0200 |
This is an automated email from the git hooks/post-receive script.
dold pushed a change to branch master
in repository libeufin.
from 236b29f9 -fix WIRE_GATEWAY_URL to match new design
new 1ce3c4d4 refactor file structure
new fb40741b refactoring, adapt to core bank API withdrawal change
The 2 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:
.../kotlin/tech/libeufin/bank/Authentication.kt | 39 ++
.../libeufin/bank/{types.kt => BankMessages.kt} | 63 ++-
.../tech/libeufin/bank/CorebankApiHandlers.kt | 456 +++++++++++++++++++++
...rationHandlers.kt => IntegrationApiHandlers.kt} | 0
bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 37 +-
...atewayHandlers.kt => WireGatewayApiHandlers.kt} | 9 +-
.../tech/libeufin/bank/accountsMgmtHandlers.kt | 168 --------
bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 290 ++++++-------
.../kotlin/tech/libeufin/bank/talerWebHandlers.kt | 177 --------
.../kotlin/tech/libeufin/bank/tokenHandlers.kt | 105 -----
.../tech/libeufin/bank/transactionsHandlers.kt | 136 ------
contrib/libeufin-bank.sample.conf | 2 +-
contrib/wallet-core | 2 +-
util/src/main/kotlin/HTTP.kt | 11 +-
14 files changed, 669 insertions(+), 826 deletions(-)
create mode 100644 bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
rename bank/src/main/kotlin/tech/libeufin/bank/{types.kt => BankMessages.kt}
(94%)
create mode 100644
bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
rename bank/src/main/kotlin/tech/libeufin/bank/{talerIntegrationHandlers.kt =>
IntegrationApiHandlers.kt} (100%)
rename bank/src/main/kotlin/tech/libeufin/bank/{talerWireGatewayHandlers.kt =>
WireGatewayApiHandlers.kt} (96%)
delete mode 100644
bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
delete mode 100644 bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
delete mode 100644 bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
delete mode 100644
bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
new file mode 100644
index 00000000..6054877e
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
@@ -0,0 +1,39 @@
+package tech.libeufin.bank
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import net.taler.common.errorcodes.TalerErrorCode
+import tech.libeufin.util.getAuthorizationDetails
+import tech.libeufin.util.getAuthorizationRawHeader
+
+/**
+ * This function tries to authenticate the call according
+ * to the scheme that is mentioned in the Authorization header.
+ * The allowed schemes are either 'HTTP basic auth' or 'bearer token'.
+ *
+ * requiredScope can be either "readonly" or "readwrite".
+ *
+ * Returns the authenticated customer, or null if they failed.
+ */
+fun ApplicationCall.authenticateBankRequest(db: Database, requiredScope:
TokenScope): Customer? {
+ // Extracting the Authorization header.
+ val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
+ "Authorization header not found.",
+ TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ val authDetails = getAuthorizationDetails(header) ?: throw badRequest(
+ "Authorization is invalid.",
+ TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ return when (authDetails.scheme) {
+ "Basic" -> doBasicAuth(db, authDetails.content)
+ "Bearer" -> doTokenAuth(db, authDetails.content, requiredScope)
+ else -> throw LibeufinBankException(
+ httpStatus = HttpStatusCode.Unauthorized,
+ talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code,
+ hint = "Authorization method wrong or not supported."
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
similarity index 94%
rename from bank/src/main/kotlin/tech/libeufin/bank/types.kt
rename to bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
index 86c5dbf7..b12292e3 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -23,17 +23,20 @@ import io.ktor.http.*
import io.ktor.server.application.*
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
-import java.io.Serial
import java.util.*
-// Allowed lengths for fractional digits in amounts.
+/**
+ * Allowed lengths for fractional digits in amounts.
+ */
enum class FracDigits(howMany: Int) {
TWO(2),
EIGHT(8)
}
-// It contains the number of microseconds since the Epoch.
+/**
+ * Timestamp containing the number of seconds since epoch.
+ */
@Serializable
data class Timestamp(
val t_s: Long // FIXME (?): not supporting "never" at the moment.
@@ -86,9 +89,10 @@ data class RegisterAccountRequest(
val internal_payto_uri: String? = null
)
-/* Internal representation of relative times. The
-* "forever" case is represented with Long.MAX_VALUE.
-*/
+/**
+ * Internal representation of relative times. The
+ * "forever" case is represented with Long.MAX_VALUE.
+ */
data class RelativeTime(
val d_us: Long
)
@@ -341,19 +345,30 @@ data class Config(
val fiat_currency: String? = null
)
-// GET /accounts/$USERNAME response.
+@Serializable
+data class Balance(
+ // FIXME: Should not be a string
+ val amount: String,
+ // FIXME: Should not be a string
+ val credit_debit_indicator: String,
+)
+
+/**
+ * GET /accounts/$USERNAME response.
+ */
@Serializable
data class AccountData(
val name: String,
- val balance: String,
+ val balance: Balance,
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
+/**
+ * Response type of corebank API transaction initiation.
+ */
@Serializable
data class BankAccountTransactionCreate(
val payto_uri: String,
@@ -457,7 +472,9 @@ data class TalerIntegrationConfigResponse(
val currency: String
)
-// Withdrawal status as spec'd in the Taler Integration API.
+/**
+ * Withdrawal status as specified in the Taler Integration API.
+ */
@Serializable
data class BankWithdrawalOperationStatus(
// Indicates whether the withdrawal was aborted.
@@ -493,7 +510,9 @@ data class BankWithdrawalOperationStatus(
val wire_types: MutableList<String> = mutableListOf("iban")
)
-// Selection request on a Taler withdrawal.
+/**
+ * Selection request on a Taler withdrawal.
+ */
@Serializable
data class BankWithdrawalOperationPostRequest(
val reserve_pub: String,
@@ -521,7 +540,9 @@ data class AddIncomingRequest(
val debit_account: String
)
-// Response to /admin/add-incoming
+/**
+ * Response to /admin/add-incoming
+ */
@Serializable
data class AddIncomingResponse(
val timestamp: Long,
@@ -535,14 +556,18 @@ data class TWGConfigResponse(
val currency: String
)
-// Response of a TWG /history/incoming call.
+/**
+ * Response of a TWG /history/incoming call.
+ */
@Serializable
data class IncomingHistory(
val incoming_transactions: MutableList<IncomingReserveTransaction> =
mutableListOf(),
val credit_account: String // Receiver's Payto URI.
)
-// TWG's incoming payment record.
+/**
+ * TWG's incoming payment record.
+ */
@Serializable
data class IncomingReserveTransaction(
val type: String = "RESERVE",
@@ -553,7 +578,9 @@ data class IncomingReserveTransaction(
val reserve_pub: String
)
-// TWG's request to pay a merchant.
+/**
+ * TWG's request to pay a merchant.
+ */
@Serializable
data class TransferRequest(
val request_uid: String,
@@ -564,7 +591,9 @@ data class TransferRequest(
val credit_account: String
)
-// TWG's response to merchant payouts
+/**
+ * TWG's response to merchant payouts
+ */
@Serializable
data class TransferResponse(
val timestamp: Long,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
new file mode 100644
index 00000000..bfae12c4
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -0,0 +1,456 @@
+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 net.taler.wallet.crypto.Base32Crockford
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import tech.libeufin.util.*
+import java.util.*
+
+private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
+
+/**
+ * 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(db: Database, ctx: BankApplicationContext) {
+
+ delete("/accounts/{USERNAME}/token") {
+ throw internalServerError("Token deletion not implemented.")
+ }
+
+ post("/accounts/{USERNAME}/token") {
+ val customer = call.authenticateBankRequest(db,
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 {
+ Random().nextBytes(this)
+ }
+ val maxDurationTime: Long = ctx.maxAuthTokenDurationUs
+ if (req.duration != null && req.duration.d_us > maxDurationTime) 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(
+ "Could not get 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 is allowed to create new accounts
+ if (ctx.restrictRegistration) {
+ val customer: Customer? = call.authenticateBankRequest(db,
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 idempotency.
+ 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 = ctx.cashoutCurrency,
+ 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 = ctx.defaultCustomerDebtLimit
+ val newBankAccount = BankAccount(
+ hasDebt = false,
+ internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
+ owningCustomerId = newCustomerRowId,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = maxDebt
+ )
+ val newBankAccountId = db.bankAccountCreate(newBankAccount)
+ ?: throw internalServerError("Could not INSERT bank account
despite all the checks.")
+
+ /**
+ * The new account got created, now optionally award the registration
+ * bonus to it. The configuration gets either a Taler amount (of the
+ * bonus), or null if no bonus is meant to be awarded.
+ */
+ val bonusAmount = if (ctx.registrationBonusEnabled)
ctx.registrationBonus else null
+ if (bonusAmount != null) {
+ val adminCustomer =
+ db.customerGetFromLogin("admin") ?: throw
internalServerError("Admin customer not found")
+ val adminBankAccount =
db.bankAccountGetFromOwnerId(adminCustomer.expectRowId())
+ ?: throw internalServerError("Admin bank account not found")
+ val adminPaysBonus = BankInternalTransaction(
+ creditorAccountId = newBankAccountId,
+ debtorAccountId = adminBankAccount.expectRowId(),
+ amount = bonusAmount,
+ subject = "Registration bonus.",
+ transactionDate = getNowUs()
+ )
+ when (db.bankTransactionCreate(adminPaysBonus)) {
+ Database.BankTransactionResult.NO_CREDITOR -> throw
internalServerError("Bonus impossible: creditor not found, despite its recent
creation.")
+
+ Database.BankTransactionResult.NO_DEBTOR -> throw
internalServerError("Bonus impossible: admin not found.")
+
+ Database.BankTransactionResult.CONFLICT -> throw
internalServerError("Bonus impossible: admin has insufficient balance.")
+
+ Database.BankTransactionResult.SUCCESS -> {/* continue the
execution */
+ }
+ }
+ }
+ call.respond(HttpStatusCode.Created)
+ return@post
+ }
+
+ get("/accounts/{USERNAME}") {
+ val c = call.authenticateBankRequest(db, 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.'")
+ val balance = Balance(
+ amount = bankAccountData.balance.toString(),
+ credit_debit_indicator = if (bankAccountData.hasDebt) { "debit" }
else { "credit" }
+ )
+ call.respond(
+ AccountData(
+ name = customerData.name,
+ balance = balance,
+ debit_threshold = bankAccountData.maxDebt.toString(),
+ payto_uri = bankAccountData.internalPaytoUri,
+ contact_data = ChallengeContactData(
+ email = customerData.email, phone = customerData.phone
+ ),
+ cashout_payto_uri = customerData.cashoutPayto,
+ )
+ )
+ return@get
+ }
+
+ post("/accounts/{USERNAME}/withdrawals") {
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?:
throw unauthorized()
+ // Admin not allowed to withdraw in the name of customers:
+ val accountName = call.expectUriComponent("USERNAME")
+ if (c.login != accountName) throw unauthorized("User ${c.login} not
allowed to withdraw for account '${accountName}'")
+ val req = call.receive<BankAccountCreateWithdrawalRequest>()
+ // Checking that the user has enough funds.
+ val b = db.bankAccountGetFromOwnerId(c.expectRowId())
+ ?: throw internalServerError("Customer '${c.login}' lacks bank
account.")
+ val withdrawalAmount = parseTalerAmount(req.amount)
+ if (!isBalanceEnough(
+ balance = b.expectBalance(), due = withdrawalAmount, maxDebt =
b.maxDebt, hasBalanceDebt = b.hasDebt
+ )
+ ) throw forbidden(
+ hint = "Insufficient funds to withdraw with Taler",
+ talerErrorCode = TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
+ )
+ // Auth and funds passed, create the operation now!
+ val opId = UUID.randomUUID()
+ if (!db.talerWithdrawalCreate(
+ opId, b.expectRowId(), withdrawalAmount
+ )
+ ) throw internalServerError("Bank failed at creating the withdraw
operation.")
+
+ val bankBaseUrl = call.request.getBaseUrl() ?: throw
internalServerError("Bank could not find its own base URL")
+ call.respond(
+ BankAccountCreateWithdrawalResponse(
+ withdrawal_id = opId.toString(), taler_withdraw_uri =
getTalerWithdrawUri(bankBaseUrl, opId.toString())
+ )
+ )
+ return@post
+ }
+
+ get("/withdrawals/{withdrawal_id}") {
+ val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
+ call.respond(
+ BankAccountGetWithdrawalResponse(
+ amount = op.amount.toString(),
+ aborted = op.aborted,
+ confirmation_done = op.confirmationDone,
+ selection_done = op.selectionDone,
+ selected_exchange_account = op.selectedExchangePayto,
+ selected_reserve_pub = op.reservePub
+ )
+ )
+ return@get
+ }
+
+ post("/withdrawals/{withdrawal_id}/abort") {
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw
unauthorized()
+ // Admin allowed to abort.
+ if (!call.getResourceName("USERNAME").canI(c)) throw forbidden()
+ val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
+ // Idempotency:
+ if (op.aborted) {
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
+ }
+ // Op is found, it'll now fail only if previously confirmed (DB
checks).
+ if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
+ hint = "Cannot abort confirmed withdrawal", talerEc =
TalerErrorCode.TALER_EC_END
+ )
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
+ }
+
+ post("/withdrawals/{withdrawal_id}/confirm") {
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?:
throw unauthorized()
+ // No admin allowed.
+ if (!call.getResourceName("USERNAME").canI(c, withAdmin = false))
throw forbidden()
+ val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
+ // Checking idempotency:
+ if (op.confirmationDone) {
+ call.respondText("{}", ContentType.Application.Json)
+ return@post
+ }
+ if (op.aborted) throw conflict(
+ hint = "Cannot confirm an aborted withdrawal", talerEc =
TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+ )
+ // Checking that reserve GOT indeed selected.
+ if (!op.selectionDone) throw LibeufinBankException(
+ httpStatus = HttpStatusCode.UnprocessableEntity, talerError =
TalerError(
+ hint = "Cannot confirm an unselected withdrawal", code =
TalerErrorCode.TALER_EC_END.code
+ )
+ )/* Confirmation conditions are all met, now put the operation
+ * to the selected state _and_ wire the funds to the exchange.
+ * Note: 'when' helps not to omit more result codes, should more
+ * be added.
+ */
+ when (db.talerWithdrawalConfirm(op.withdrawalUuid, getNowUs())) {
+ WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw
conflict(
+ "Insufficient funds", TalerErrorCode.TALER_EC_END // FIXME:
define EC for this.
+ )
+
+ WithdrawalConfirmationResult.OP_NOT_FOUND ->
+ /**
+ * Despite previous checks, the database _still_ did not
+ * find the withdrawal operation, that's on the bank.
+ */
+ throw internalServerError("Withdrawal operation
(${op.withdrawalUuid}) not found")
+
+ WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND ->
+ /**
+ * That can happen because the bank did not check the exchange
+ * exists when POST /withdrawals happened, or because the
exchange
+ * bank account got removed before this confirmation.
+ */
+ throw conflict(
+ hint = "Exchange to withdraw from not found", talerEc =
TalerErrorCode.TALER_EC_END // FIXME
+ )
+
+ WithdrawalConfirmationResult.CONFLICT -> throw
internalServerError("Bank didn't check for idempotency")
+
+ WithdrawalConfirmationResult.SUCCESS -> call.respondText(
+ "{}", ContentType.Application.Json
+ )
+ }
+ return@post
+ }
+
+ get("/accounts/{USERNAME}/transactions") {
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw
unauthorized()
+ val resourceName = call.expectUriComponent("USERNAME")
+ if (c.login != resourceName && c.login != "admin") throw forbidden()
+ // Collecting params.
+ val historyParams = getHistoryParams(call.request)
+ // Making the query.
+ val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
+ ?: throw internalServerError("Customer '${c.login}' lacks bank
account.")
+ val bankAccountId = bankAccount.expectRowId()
+ val history: List<BankAccountTransaction> =
db.bankTransactionGetHistory(
+ start = historyParams.start, delta = historyParams.delta,
bankAccountId = bankAccountId
+ )
+ val res = BankAccountTransactionsResponse(transactions =
mutableListOf())
+ history.forEach {
+ res.transactions.add(
+ BankAccountTransactionInfo(
+ debtor_payto_uri = it.debtorPaytoUri,
+ creditor_payto_uri = it.creditorPaytoUri,
+ subject = it.subject,
+ amount = it.amount.toString(),
+ direction = it.direction,
+ date = it.transactionDate,
+ row_id = it.dbRowId ?: throw internalServerError(
+ "Transaction timestamped with '${it.transactionDate}'
did not have row ID"
+ )
+ )
+ )
+ }
+ call.respond(res)
+ return@get
+ }
+
+ // Creates a bank transaction.
+ post("/accounts/{USERNAME}/transactions") {
+ val c = call.authenticateBankRequest(db, 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 != ctx.currency) 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.authenticateBankRequest(db, 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/talerIntegrationHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt
similarity index 100%
rename from bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt
rename to bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 61ea1227..cb4bb7f9 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -165,38 +165,6 @@ object TalerAmountSerializer : KSerializer<TalerAmount> {
}
}
-/**
- * This function tries to authenticate the call according
- * to the scheme that is mentioned in the Authorization header.
- * The allowed schemes are either 'HTTP basic auth' or 'bearer token'.
- *
- * requiredScope can be either "readonly" or "readwrite".
- *
- * Returns the authenticated customer, or null if they failed.
- */
-fun ApplicationCall.myAuth(db: Database, requiredScope: TokenScope): Customer?
{
- // Extracting the Authorization header.
- val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
- "Authorization header not found.",
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
- )
- val authDetails = getAuthorizationDetails(header) ?: throw badRequest(
- "Authorization is invalid.",
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
- )
- return when (authDetails.scheme) {
- "Basic" -> doBasicAuth(db, authDetails.content)
- "Bearer" -> doTokenAuth(db, authDetails.content, requiredScope)
- else -> throw LibeufinBankException(
- httpStatus = HttpStatusCode.Unauthorized,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code,
- hint = "Authorization method wrong or not supported."
- )
- )
- }
-}
-
/**
* Set up web server handlers for the Taler corebank API.
@@ -311,9 +279,6 @@ fun Application.corebankWebApp(db: Database, ctx:
BankApplicationContext) {
return@get
}
this.accountsMgmtHandlers(db, ctx)
- this.tokenHandlers(db, ctx)
- this.transactionsHandlers(db, ctx)
- this.talerWebHandlers(db)
this.talerIntegrationHandlers(db, ctx)
this.talerWireGatewayHandlers(db, ctx)
}
@@ -411,7 +376,7 @@ fun readBankApplicationContextFromConfig(cfg: TalerConfig):
BankApplicationConte
class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name =
"serve") {
private val configFile by option(
- "--config",
+ "--config", "-c",
help = "set the configuration file"
)
init {
diff --git
a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
similarity index 96%
rename from bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
rename to bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index 619e5fc8..dda207b5 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -34,8 +34,9 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
call.respond(TWGConfigResponse(currency = ctx.currency))
return@get
}
+
get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw
unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = true)) throw
forbidden()
val params = getHistoryParams(call.request)
val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
@@ -66,8 +67,9 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
call.respond(resp)
return@get
}
+
post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?:
throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = false))
throw forbidden()
val req = call.receive<TransferRequest>()
// Checking for idempotency.
@@ -120,8 +122,9 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx:
BankApplicationContext)
))
return@post
}
+
post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?:
throw unauthorized()
if (!call.getResourceName("USERNAME").canI(c, withAdmin = false))
throw forbidden()
val req = call.receive<AddIncomingRequest>()
val amount = parseTalerAmount(req.amount)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
deleted file mode 100644
index ef6d66e3..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/accountsMgmtHandlers.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-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 org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.getNowUs
-import tech.libeufin.util.maybeUriComponent
-
-private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
-
-/**
- * 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(db: Database, ctx: BankApplicationContext) {
- post("/accounts") {
- // check if only admin is allowed to create new accounts
- if (ctx.restrictRegistration) {
- val customer: Customer? = call.myAuth(db, 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 idempotency.
- 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 = ctx.cashoutCurrency,
- 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 = ctx.defaultCustomerDebtLimit
- val newBankAccount = BankAccount(
- hasDebt = false,
- internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
- owningCustomerId = newCustomerRowId,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = maxDebt
- )
- val newBankAccountId = db.bankAccountCreate(newBankAccount)
- ?: throw internalServerError("Could not INSERT bank account
despite all the checks.")
-
- /**
- * The new account got created, now optionally award the registration
- * bonus to it. The configuration gets either a Taler amount (of the
- * bonus), or null if no bonus is meant to be awarded.
- */
- val bonusAmount = if (ctx.registrationBonusEnabled)
ctx.registrationBonus else null
- if (bonusAmount != null) {
- val adminCustomer = db.customerGetFromLogin("admin")
- ?: throw internalServerError("Admin customer not found")
- val adminBankAccount =
db.bankAccountGetFromOwnerId(adminCustomer.expectRowId())
- ?: throw internalServerError("Admin bank account not found")
- val adminPaysBonus = BankInternalTransaction(
- creditorAccountId = newBankAccountId,
- debtorAccountId = adminBankAccount.expectRowId(),
- amount = bonusAmount,
- subject = "Registration bonus.",
- transactionDate = getNowUs()
- )
- when(db.bankTransactionCreate(adminPaysBonus)) {
- Database.BankTransactionResult.NO_CREDITOR ->
- throw internalServerError("Bonus impossible: creditor not
found, despite its recent creation.")
- Database.BankTransactionResult.NO_DEBTOR ->
- throw internalServerError("Bonus impossible: admin not
found.")
- Database.BankTransactionResult.CONFLICT ->
- throw internalServerError("Bonus impossible: admin has
insufficient balance.")
- Database.BankTransactionResult.SUCCESS -> {/* continue the
execution */}
- }
- }
- call.respond(HttpStatusCode.Created)
- return@post
- }
- get("/accounts/{USERNAME}") {
- val c = call.myAuth(db, 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 dd7d8027..9969f32c 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -20,7 +20,6 @@
package tech.libeufin.bank
import io.ktor.http.*
-import io.ktor.http.cio.*
import io.ktor.server.application.*
import io.ktor.server.plugins.*
import io.ktor.server.request.*
@@ -30,10 +29,8 @@ import net.taler.wallet.crypto.Base32Crockford
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.*
-import java.lang.NumberFormatException
import java.net.URL
import java.util.*
-import kotlin.system.exitProcess
const val FRACTION_BASE = 100000000
@@ -41,23 +38,20 @@ private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.helpers
fun ApplicationCall.expectUriComponent(componentName: String) =
this.maybeUriComponent(componentName) ?: throw badRequest(
- hint = "No username found in the URI",
- talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
-)
+ 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? {
val h = getAuthorizationRawHeader(this.request) ?: return null
val authDetails = getAuthorizationDetails(h) ?: throw badRequest(
- "Authorization header is malformed.",
+ "Authorization header is malformed.",
TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ if (authDetails.scheme == "Bearer") return
splitBearerToken(authDetails.content) ?: throw throw badRequest(
+ "Authorization header is malformed (could not strip the prefix from
Bearer token).",
TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
)
- if (authDetails.scheme == "Bearer")
- return splitBearerToken(authDetails.content) ?: throw
- throw badRequest(
- "Authorization header is malformed (could not strip the prefix
from Bearer token).",
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
- )
return null // Not a Bearer token case.
}
@@ -76,14 +70,12 @@ fun doBasicAuth(db: Database, encodedCredentials: String):
Customer? {
*/
limit = 2
)
- if (userAndPassSplit.size != 2)
- throw LibeufinBankException(
- httpStatus = HttpStatusCode.BadRequest,
- talerError = TalerError(
- code =
TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED.code,
- "Malformed Basic auth credentials found in the Authorization
header."
- )
+ if (userAndPassSplit.size != 2) throw LibeufinBankException(
+ httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED.code,
+ "Malformed Basic auth credentials found in the Authorization
header."
)
+ )
val login = userAndPassSplit[0]
val plainPassword = userAndPassSplit[1]
val maybeCustomer = db.customerGetFromLogin(login) ?: throw unauthorized()
@@ -111,15 +103,13 @@ fun doTokenAuth(
requiredScope: TokenScope,
): Customer? {
val bareToken = splitBearerToken(token) ?: throw badRequest(
- "Bearer token malformed",
- talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ "Bearer token malformed", talerErrorCode =
TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
)
val tokenBytes = try {
Base32Crockford.decode(bareToken)
} catch (e: Exception) {
throw badRequest(
- e.message,
- TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+ e.message, TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
)
}
val maybeToken: BearerToken? = db.bearerTokenGet(tokenBytes)
@@ -140,87 +130,67 @@ fun doTokenAuth(
return null
}
// Getting the related username.
- return db.customerGetFromRowId(maybeToken.bankCustomer)
- ?: throw LibeufinBankException(
- httpStatus = HttpStatusCode.InternalServerError,
- talerError = TalerError(
- code =
TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
- hint = "Customer not found, despite token mentions it.",
- ))
+ return db.customerGetFromRowId(maybeToken.bankCustomer) ?: throw
LibeufinBankException(
+ httpStatus = HttpStatusCode.InternalServerError, talerError =
TalerError(
+ code =
TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
+ hint = "Customer not found, despite token mentions it.",
+ )
+ )
}
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(
- code = talerErrorCode.code,
- hint = hint
- )
+): LibeufinBankException = LibeufinBankException(
+ httpStatus = HttpStatusCode.Forbidden, talerError = TalerError(
+ code = talerErrorCode.code, hint = hint
)
+)
-fun unauthorized(hint: String = "Login failed"): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.Unauthorized,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code,
- hint = hint
- )
+fun unauthorized(hint: String = "Login failed"): LibeufinBankException =
LibeufinBankException(
+ httpStatus = HttpStatusCode.Unauthorized, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_BANK_LOGIN_FAILED.code, hint = hint
)
-fun internalServerError(hint: String?): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.InternalServerError,
- talerError = TalerError(
- code =
TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
- hint = hint
- )
+)
+
+fun internalServerError(hint: String?): LibeufinBankException =
LibeufinBankException(
+ httpStatus = HttpStatusCode.InternalServerError, talerError = TalerError(
+ code =
TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code, hint = hint
)
+)
fun notFound(
- hint: String?,
- talerEc: TalerErrorCode
-): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.NotFound,
- talerError = TalerError(
- code = talerEc.code,
- hint = hint
- )
+ 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
- )
+ 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
-): LibeufinBankException =
- LibeufinBankException(
- httpStatus = HttpStatusCode.BadRequest,
- talerError = TalerError(
- code = talerErrorCode.code,
- hint = hint
- )
+ hint: String? = null, talerErrorCode: TalerErrorCode =
TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
+): LibeufinBankException = LibeufinBankException(
+ httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
+ code = talerErrorCode.code, hint = hint
)
+)
+
// Generates a new Payto-URI with IBAN scheme.
fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}"
// Parses Taler amount, returning null if the input is invalid.
fun parseTalerAmount2(
- amount: String,
- fracDigits: FracDigits
+ amount: String, fracDigits: FracDigits
): TalerAmount? {
val format = when (fracDigits) {
FracDigits.TWO -> "^([A-Z]+):([0-9]+)(\\.[0-9][0-9]?)?$"
@@ -246,11 +216,10 @@ fun parseTalerAmount2(
return null
}
return TalerAmount(
- value = value,
- frac = fraction,
- currency = match.destructured.component1()
+ value = value, frac = fraction, currency =
match.destructured.component1()
)
}
+
/**
* This helper takes the serialized version of a Taler Amount
* type and parses it into Libeufin's internal representation.
@@ -260,16 +229,13 @@ fun parseTalerAmount2(
* responded to the client.
*/
fun parseTalerAmount(
- amount: String,
- fracDigits: FracDigits = FracDigits.EIGHT
+ amount: String, fracDigits: FracDigits = FracDigits.EIGHT
): TalerAmount {
- val maybeAmount = parseTalerAmount2(amount, fracDigits)
- ?: throw LibeufinBankException(
- httpStatus = HttpStatusCode.BadRequest,
- talerError = TalerError(
- code = TalerErrorCode.TALER_EC_BANK_BAD_FORMAT_AMOUNT.code,
- hint = "Invalid amount: $amount"
- ))
+ val maybeAmount = parseTalerAmount2(amount, fracDigits) ?: throw
LibeufinBankException(
+ httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
+ code = TalerErrorCode.TALER_EC_BANK_BAD_FORMAT_AMOUNT.code, hint =
"Invalid amount: $amount"
+ )
+ )
return maybeAmount
}
@@ -278,9 +244,7 @@ private fun normalizeAmount(amt: TalerAmount): TalerAmount {
val normalValue = amt.value + (amt.frac / FRACTION_BASE)
val normalFrac = amt.frac % FRACTION_BASE
return TalerAmount(
- value = normalValue,
- frac = normalFrac,
- currency = amt.currency
+ value = normalValue, frac = normalFrac, currency = amt.currency
)
}
return amt
@@ -289,22 +253,19 @@ private fun normalizeAmount(amt: TalerAmount):
TalerAmount {
// Adds two amounts and returns the normalized version.
private fun amountAdd(first: TalerAmount, second: TalerAmount): TalerAmount {
- if (first.currency != second.currency)
- throw badRequest(
- "Currency mismatch, balance '${first.currency}', price
'${second.currency}'",
- TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
- )
+ if (first.currency != second.currency) throw badRequest(
+ "Currency mismatch, balance '${first.currency}', price
'${second.currency}'",
+ TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
val valueAdd = first.value + second.value
- if (valueAdd < first.value)
- throw badRequest("Amount value overflowed")
+ if (valueAdd < first.value) throw badRequest("Amount value overflowed")
val fracAdd = first.frac + second.frac
- if (fracAdd < first.frac)
- throw badRequest("Amount fraction overflowed")
- return normalizeAmount(TalerAmount(
- value = valueAdd,
- frac = fracAdd,
- currency = first.currency
- ))
+ if (fracAdd < first.frac) throw badRequest("Amount fraction overflowed")
+ return normalizeAmount(
+ TalerAmount(
+ value = valueAdd, frac = fracAdd, currency = first.currency
+ )
+ )
}
/**
@@ -315,20 +276,13 @@ private fun amountAdd(first: TalerAmount, second:
TalerAmount): TalerAmount {
* the database.
*/
fun isBalanceEnough(
- balance: TalerAmount,
- due: TalerAmount,
- maxDebt: TalerAmount,
- hasBalanceDebt: Boolean
+ balance: TalerAmount, due: TalerAmount, maxDebt: TalerAmount,
hasBalanceDebt: Boolean
): Boolean {
val normalMaxDebt = normalizeAmount(maxDebt) // Very unlikely to be needed.
if (hasBalanceDebt) {
val chargedBalance = amountAdd(balance, due)
if (chargedBalance.value > normalMaxDebt.value) return false // max
debt surpassed
- if (
- (chargedBalance.value == normalMaxDebt.value) &&
- (chargedBalance.frac > maxDebt.frac)
- )
- return false
+ if ((chargedBalance.value == normalMaxDebt.value) &&
(chargedBalance.frac > maxDebt.frac)) return false
return true
}
/**
@@ -336,19 +290,17 @@ fun isBalanceEnough(
* block calculates how much debt the balance would get, should a
* subtraction of 'due' occur.
*/
- if (balance.currency != due.currency)
- throw badRequest(
- "Currency mismatch, balance '${balance.currency}', due
'${due.currency}'",
- TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
- )
+ if (balance.currency != due.currency) throw badRequest(
+ "Currency mismatch, balance '${balance.currency}', due
'${due.currency}'",
+ TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
val valueDiff = if (balance.value < due.value) due.value - balance.value
else 0L
val fracDiff = if (balance.frac < due.frac) due.frac - balance.frac else 0
// Getting the normalized version of such diff.
val normalDiff = normalizeAmount(TalerAmount(valueDiff, fracDiff,
balance.currency))
// Failing if the normalized diff surpasses the max debt.
if (normalDiff.value > normalMaxDebt.value) return false
- if ((normalDiff.value == normalMaxDebt.value) &&
- (normalDiff.frac > normalMaxDebt.frac)) return false
+ if ((normalDiff.value == normalMaxDebt.value) && (normalDiff.frac >
normalMaxDebt.frac)) return false
return true
}
@@ -360,47 +312,41 @@ fun isBalanceEnough(
*
* https://$BANK_URL/taler-integration
*/
-fun getTalerWithdrawUri(baseUrl: String, woId: String) =
- url {
- val baseUrlObj = URL(baseUrl)
- protocol = URLProtocol(
- name = "taler".plus(if (baseUrlObj.protocol.lowercase() == "http")
"+http" else ""),
- defaultPort = -1
- )
- host = "withdraw"
- val pathSegments = mutableListOf(
- // adds the hostname(+port) of the actual bank that will serve the
withdrawal request.
- baseUrlObj.host.plus(
- if (baseUrlObj.port != -1)
- ":${baseUrlObj.port}"
- else ""
- )
+fun getTalerWithdrawUri(baseUrl: String, woId: String) = url {
+ val baseUrlObj = URL(baseUrl)
+ protocol = URLProtocol(
+ name = "taler".plus(if (baseUrlObj.protocol.lowercase() == "http")
"+http" else ""), defaultPort = -1
+ )
+ host = "withdraw"
+ val pathSegments = mutableListOf(
+ // adds the hostname(+port) of the actual bank that will serve the
withdrawal request.
+ baseUrlObj.host.plus(
+ if (baseUrlObj.port != -1) ":${baseUrlObj.port}"
+ else ""
)
- // Removing potential double slashes.
- baseUrlObj.path.split("/").forEach {
- if (it.isNotEmpty()) pathSegments.add(it)
- }
- pathSegments.add("taler-integration/${woId}")
- this.appendPathSegments(pathSegments)
+ )
+ // Removing potential double slashes.
+ baseUrlObj.path.split("/").forEach {
+ if (it.isNotEmpty()) pathSegments.add(it)
}
+ pathSegments.add("taler-integration/${woId}")
+ this.appendPathSegments(pathSegments)
+}
// Builds a withdrawal confirm URL.
fun getWithdrawalConfirmUrl(
- baseUrl: String,
- wopId: String,
- username: String
- ) =
- url {
- val baseUrlObj = URL(baseUrl)
- protocol = URLProtocol(name = baseUrlObj.protocol, defaultPort = -1)
- host = baseUrlObj.host
- // Removing potential double slashes:
- baseUrlObj.path.split("/").forEach {
- this.appendPathSegments(it)
- }
- // Completing the endpoint:
-
this.appendPathSegments("accounts/${username}/withdrawals/${wopId}/confirm")
+ baseUrl: String, wopId: String, username: String
+) = url {
+ val baseUrlObj = URL(baseUrl)
+ protocol = URLProtocol(name = baseUrlObj.protocol, defaultPort = -1)
+ host = baseUrlObj.host
+ // Removing potential double slashes:
+ baseUrlObj.path.split("/").forEach {
+ this.appendPathSegments(it)
}
+ // Completing the endpoint:
+
this.appendPathSegments("accounts/${username}/withdrawals/${wopId}/confirm")
+}
/**
@@ -417,24 +363,23 @@ fun getWithdrawal(db: Database, opIdParam: String):
TalerWithdrawalOperation {
logger.error(e.message)
throw badRequest("withdrawal_id query parameter was malformed")
}
- val op = db.talerWithdrawalGet(opId)
- ?: throw notFound(
- hint = "Withdrawal operation $opIdParam not found",
- talerEc = TalerErrorCode.TALER_EC_END
- )
+ val op = db.talerWithdrawalGet(opId) ?: throw notFound(
+ hint = "Withdrawal operation $opIdParam not found", talerEc =
TalerErrorCode.TALER_EC_END
+ )
return op
}
data class HistoryParams(
- val delta: Long,
- val start: Long
+ val delta: Long, val start: Long
)
+
/**
* Extracts the query parameters from "history-like" endpoints,
* providing the defaults according to the API specification.
*/
fun getHistoryParams(req: ApplicationRequest): HistoryParams {
- val deltaParam: String = req.queryParameters["delta"] ?: throw
MissingRequestParameterException(parameterName = "delta")
+ val deltaParam: String =
+ req.queryParameters["delta"] ?: throw
MissingRequestParameterException(parameterName = "delta")
val delta: Long = try {
deltaParam.toLong()
} catch (e: Exception) {
@@ -473,8 +418,7 @@ fun maybeCreateAdminAccount(db: Database, ctx:
BankApplicationContext): Boolean
* Hashing the password helps to avoid the "password not hashed"
* error, in case the admin tries to authenticate.
*/
- passwordHash = CryptoUtil.hashpw(String(pwBuf, Charsets.UTF_8)),
- name = "Bank administrator"
+ passwordHash = CryptoUtil.hashpw(String(pwBuf, Charsets.UTF_8)),
name = "Bank administrator"
)
val rowId = db.customerCreate(adminCustomer)
if (rowId == null) {
@@ -482,9 +426,7 @@ fun maybeCreateAdminAccount(db: Database, ctx:
BankApplicationContext): Boolean
return false
}
rowId
- }
- else
- maybeAdminCustomer.expectRowId()
+ } else maybeAdminCustomer.expectRowId()
val maybeAdminBankAccount = db.bankAccountGetFromOwnerId(adminCustomerId)
if (maybeAdminBankAccount == null) {
logger.info("Creating admin bank account")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
deleted file mode 100644
index 258bc005..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-/* This file contains all the Taler handlers that do NOT
- * communicate with wallets, therefore any handler that serves
- * to SPAs or CLI HTTP clients.
- */
-
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-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.getBaseUrl
-import tech.libeufin.util.getNowUs
-import java.util.*
-
-fun Routing.talerWebHandlers(db: Database) {
- post("/accounts/{USERNAME}/withdrawals") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
- // Admin not allowed to withdraw in the name of customers:
- val accountName = call.expectUriComponent("USERNAME")
- if (c.login != accountName)
- throw unauthorized("User ${c.login} not allowed to withdraw for
account '${accountName}'")
- val req = call.receive<BankAccountCreateWithdrawalRequest>()
- // Checking that the user has enough funds.
- val b = db.bankAccountGetFromOwnerId(c.expectRowId())
- ?: throw internalServerError("Customer '${c.login}' lacks bank
account.")
- val withdrawalAmount = parseTalerAmount(req.amount)
- if (
- !isBalanceEnough(
- balance = b.expectBalance(),
- due = withdrawalAmount,
- maxDebt = b.maxDebt,
- hasBalanceDebt = b.hasDebt
- ))
- throw forbidden(
- hint = "Insufficient funds to withdraw with Taler",
- talerErrorCode = TalerErrorCode.TALER_EC_NONE // FIXME: need
EC.
- )
- // Auth and funds passed, create the operation now!
- val opId = UUID.randomUUID()
- if(
- !db.talerWithdrawalCreate(
- opId,
- b.expectRowId(),
- withdrawalAmount
- )
- )
- throw internalServerError("Bank failed at creating the withdraw
operation.")
-
- val bankBaseUrl = call.request.getBaseUrl()
- ?: throw internalServerError("Bank could not find its own base
URL")
- call.respond(BankAccountCreateWithdrawalResponse(
- withdrawal_id = opId.toString(),
- taler_withdraw_uri = getTalerWithdrawUri(bankBaseUrl,
opId.toString())
- ))
- return@post
- }
- get("/accounts/{USERNAME}/withdrawals/{withdrawal_id}") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
- val accountName = call.expectUriComponent("USERNAME")
- // Admin allowed to see the details
- if (c.login != accountName && c.login != "admin") throw forbidden()
- // Permissions passed, get the information.
- val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
- call.respond(BankAccountGetWithdrawalResponse(
- amount = op.amount.toString(),
- aborted = op.aborted,
- confirmation_done = op.confirmationDone,
- selection_done = op.selectionDone,
- selected_exchange_account = op.selectedExchangePayto,
- selected_reserve_pub = op.reservePub
- ))
- return@get
- }
- post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
- // Admin allowed to abort.
- if (!call.getResourceName("USERNAME").canI(c)) throw forbidden()
- val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
- // Idempotency:
- if (op.aborted) {
- call.respondText("{}", ContentType.Application.Json)
- return@post
- }
- // Op is found, it'll now fail only if previously confirmed (DB
checks).
- if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
- hint = "Cannot abort confirmed withdrawal",
- talerEc = TalerErrorCode.TALER_EC_END
- )
- call.respondText("{}", ContentType.Application.Json)
- return@post
- }
- post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
- // No admin allowed.
- if(!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw
forbidden()
- val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
- // Checking idempotency:
- if (op.confirmationDone) {
- call.respondText("{}", ContentType.Application.Json)
- return@post
- }
- if (op.aborted)
- throw conflict(
- hint = "Cannot confirm an aborted withdrawal",
- talerEc = TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
- )
- // Checking that reserve GOT indeed selected.
- if (!op.selectionDone)
- throw LibeufinBankException(
- httpStatus = HttpStatusCode.UnprocessableEntity,
- talerError = TalerError(
- hint = "Cannot confirm an unselected withdrawal",
- code = TalerErrorCode.TALER_EC_END.code
- ))
- /* Confirmation conditions are all met, now put the operation
- * to the selected state _and_ wire the funds to the exchange.
- * Note: 'when' helps not to omit more result codes, should more
- * be added.
- */
- when (db.talerWithdrawalConfirm(op.withdrawalUuid, getNowUs())) {
- WithdrawalConfirmationResult.BALANCE_INSUFFICIENT ->
- throw conflict(
- "Insufficient funds",
- TalerErrorCode.TALER_EC_END // FIXME: define EC for this.
- )
- WithdrawalConfirmationResult.OP_NOT_FOUND ->
- /**
- * Despite previous checks, the database _still_ did not
- * find the withdrawal operation, that's on the bank.
- */
- throw internalServerError("Withdrawal operation
(${op.withdrawalUuid}) not found")
- WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND ->
- /**
- * That can happen because the bank did not check the exchange
- * exists when POST /withdrawals happened, or because the
exchange
- * bank account got removed before this confirmation.
- */
- throw conflict(
- hint = "Exchange to withdraw from not found",
- talerEc = TalerErrorCode.TALER_EC_END // FIXME
- )
- WithdrawalConfirmationResult.CONFLICT ->
- throw internalServerError("Bank didn't check for idempotency")
- WithdrawalConfirmationResult.SUCCESS ->
- call.respondText(
- "{}",
- ContentType.Application.Json
- )
- }
- return@post
- }
-}
-
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
deleted file mode 100644
index 218651d3..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2019 Stanisci and Dold.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-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 org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.maybeUriComponent
-import tech.libeufin.util.getNowUs
-
-private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
-
-fun Routing.tokenHandlers(db: Database, ctx: BankApplicationContext) {
- delete("/accounts/{USERNAME}/token") {
- throw internalServerError("Token deletion not implemented.")
- }
- post("/accounts/{USERNAME}/token") {
- val customer = call.myAuth(db, 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 = ctx.maxAuthTokenDurationUs
- if (req.duration != null && req.duration.d_us > maxDurationTime)
- 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(
- "Could not get 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
deleted file mode 100644
index 64266110..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package tech.libeufin.bank
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.plugins.*
-import io.ktor.server.request.*
-import io.ktor.server.response.*
-import io.ktor.server.routing.*
-import net.taler.common.errorcodes.TalerErrorCode
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.getNowUs
-import tech.libeufin.util.parsePayto
-import kotlin.math.abs
-
-
-private val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank.transactionHandlers")
-
-fun Routing.transactionsHandlers(db: Database, ctx: BankApplicationContext) {
- get("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
- val resourceName = call.expectUriComponent("USERNAME")
- if (c.login != resourceName && c.login != "admin") throw forbidden()
- // Collecting params.
- val historyParams = getHistoryParams(call.request)
- // Making the query.
- val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
- ?: throw internalServerError("Customer '${c.login}' lacks bank
account.")
- val bankAccountId = bankAccount.expectRowId()
- val history: List<BankAccountTransaction> =
db.bankTransactionGetHistory(
- start = historyParams.start,
- delta = historyParams.delta,
- bankAccountId = bankAccountId
- )
- val res = BankAccountTransactionsResponse(transactions =
mutableListOf())
- history.forEach {
- res.transactions.add(BankAccountTransactionInfo(
- debtor_payto_uri = it.debtorPaytoUri,
- creditor_payto_uri = it.creditorPaytoUri,
- subject = it.subject,
- amount = it.amount.toString(),
- direction = it.direction,
- date = it.transactionDate,
- row_id = it.dbRowId ?: throw internalServerError(
- "Transaction timestamped with '${it.transactionDate}' did
not have row ID"
- )
- ))
- }
- call.respond(res)
- return@get
- }
- // Creates a bank transaction.
- post("/accounts/{USERNAME}/transactions") {
- val c = call.myAuth(db, 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 != ctx.currency)
- 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(db, 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/contrib/libeufin-bank.sample.conf
b/contrib/libeufin-bank.sample.conf
index a50fef97..00317b8e 100644
--- a/contrib/libeufin-bank.sample.conf
+++ b/contrib/libeufin-bank.sample.conf
@@ -1,5 +1,5 @@
[libeufin-bank]
-currency = KUDOS
+CURRENCY = KUDOS
DEFAULT_CUSTOMER_DEBT_LIMIT = KUDOS:200
DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:2000
REGISTRATION_BONUS = KUDOS:100
diff --git a/contrib/wallet-core b/contrib/wallet-core
index 7079bce1..9e2d95b3 160000
--- a/contrib/wallet-core
+++ b/contrib/wallet-core
@@ -1 +1 @@
-Subproject commit 7079bce1ad2640e44561f56b46d5f00758df8e5d
+Subproject commit 9e2d95b39723a038eb714d723ac0910a5bf596e2
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index ac3a51bc..b1e0fc02 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -3,9 +3,7 @@ package tech.libeufin.util
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
-import io.ktor.server.response.*
import io.ktor.server.util.*
-import io.ktor.util.*
import logger
// Get the base URL of a request, returns null if any problem occurs.
@@ -22,13 +20,13 @@ fun ApplicationRequest.getBaseUrl(): String? {
prefix += "/"
URLBuilder(
protocol = URLProtocol(
- name = this.headers.get("X-Forwarded-Proto") ?: run {
+ name = this.headers["X-Forwarded-Proto"] ?: run {
logger.error("Reverse proxy did not define
X-Forwarded-Proto")
return null
},
defaultPort = -1 // Port must be specified with
X-Forwarded-Host.
),
- host = this.headers.get("X-Forwarded-Host") ?: run {
+ host = this.headers["X-Forwarded-Host"] ?: run {
logger.error("Reverse proxy did not define X-Forwarded-Host")
return null
}
@@ -46,10 +44,6 @@ 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.maybeUriComponent(name: String): String? {
val ret: String? = this.parameters[name]
if (ret == null) {
@@ -77,6 +71,7 @@ data class AuthorizationDetails(
val scheme: String,
val content: String
)
+
// Returns the authorization scheme mentioned in the Auth header,
// or null if that could not be found.
fun getAuthorizationDetails(authorizationHeader: String):
AuthorizationDetails? {
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
- [libeufin] branch master updated (236b29f9 -> fb40741b),
gnunet <=