[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] 02/02: refactoring, adapt to core bank API withdrawal change
From: |
gnunet |
Subject: |
[libeufin] 02/02: refactoring, adapt to core bank API withdrawal change |
Date: |
Sun, 24 Sep 2023 14:18:56 +0200 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository libeufin.
commit fb40741be92b179dba8fc416f993c59f1a6850aa
Author: Florian Dold <florian@dold.me>
AuthorDate: Sun Sep 24 14:18:56 2023 +0200
refactoring, adapt to core bank API withdrawal change
---
.../kotlin/tech/libeufin/bank/Authentication.kt | 39 ++
.../libeufin/bank/{types.kt => BankMessages.kt} | 63 +++-
.../tech/libeufin/bank/CorebankApiHandlers.kt | 404 ++++++++++-----------
bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 34 +-
.../tech/libeufin/bank/WireGatewayApiHandlers.kt | 9 +-
bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 290 ++++++---------
contrib/libeufin-bank.sample.conf | 2 +-
util/src/main/kotlin/HTTP.kt | 11 +-
8 files changed, 395 insertions(+), 457 deletions(-)
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
index 49421402..bfae12c4 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -24,15 +24,13 @@ fun Routing.accountsMgmtHandlers(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 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
- )
+ 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>()
/**
@@ -44,33 +42,29 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
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
- )
+ 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)
+ 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
+ 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
- )
+ 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,
@@ -79,12 +73,10 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
scope = req.scope,
isRefreshable = req.refreshable
)
- if (!db.bearerTokenCreate(token))
- throw internalServerError("Failed at inserting new token in the
database")
+ if (!db.bearerTokenCreate(token)) throw internalServerError("Failed at
inserting new token in the database")
call.respond(
TokenSuccessResponse(
- access_token = Base32Crockford.encode(tokenBytes),
- expiration = Timestamp(
+ access_token = Base32Crockford.encode(tokenBytes), expiration
= Timestamp(
t_s = expirationTimestampUs / 1000000L
)
)
@@ -95,27 +87,23 @@ 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."
- )
+ 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."
- )
+ 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.
@@ -124,24 +112,19 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
db.bankAccountGetFromOwnerId(this.expectRowId())
}
if (maybeCustomerExists != null && maybeHasBankAccount != null) {
- tech.libeufin.bank.logger.debug("Registering username was found:
${maybeCustomerExists.login}")
+ 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
+ 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(
+ httpStatus = HttpStatusCode.Conflict, talerError = TalerError(
code = GENERIC_UNDEFINED, // GANA needs this.
hint = "Idempotency check failed."
)
@@ -159,8 +142,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
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
+ ?: 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
@@ -182,8 +164,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
*/
val bonusAmount = if (ctx.registrationBonusEnabled)
ctx.registrationBonus else null
if (bonusAmount != null) {
- val adminCustomer = db.customerGetFromLogin("admin")
- ?: throw internalServerError("Admin customer not found")
+ 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(
@@ -193,111 +175,104 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
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 */}
+ 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 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
+ 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)
+ 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,
)
- 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
}
post("/accounts/{USERNAME}/withdrawals") {
- val c = call.myAuth(db, TokenScope.readwrite) ?: throw unauthorized()
+ 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}'")
+ 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.
+ 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
+ if (!db.talerWithdrawalCreate(
+ opId, b.expectRowId(), withdrawalAmount
)
- )
- throw internalServerError("Bank failed at creating the withdraw
operation.")
+ ) 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())
- ))
+ 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.
+
+ 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
- ))
+ 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()
+
+ 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"))
@@ -308,52 +283,47 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
}
// 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
+ 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()
+
+ 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()
+ 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
- )
+ 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
+ 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.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
@@ -361,22 +331,20 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
* 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
+ 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.myAuth(db, TokenScope.readonly) ?: throw unauthorized()
+ 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.
@@ -386,34 +354,34 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
?: 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
+ 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"
+ 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 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()
+ 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")
@@ -422,17 +390,13 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
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 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
- )
+ 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,
@@ -441,26 +405,25 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
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.")
+ 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 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()
+ if (c.login != "admin" && c.login != accountOwner) throw forbidden()
// rights ok, check tx exists.
val tId = call.expectUriComponent("T_ID")
val txRowId = try {
@@ -470,25 +433,24 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx:
BankApplicationContext) {
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 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")
+ 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
- ))
+ 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/Main.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index c01fd67f..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.
@@ -408,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/WireGatewayApiHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index 619e5fc8..dda207b5 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.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/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/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/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.