libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 73bb6bfaf25393d125bfafb37bae03df95592510
parent 86e5751ef11e9317e66b5c35ed94552ec498ada1
Author: MS <ms@taler.net>
Date:   Tue,  3 Oct 2023 15:41:52 +0200

Progress with PATCH /accounts/{USERNAME}.

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt | 15+++++++++++++++
Mbank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mbank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt | 14+++++++++-----
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 3++-
4 files changed, 99 insertions(+), 32 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt @@ -668,7 +668,21 @@ data class PublicAccount( val account_name: String ) +/** + * Request of PATCH /accounts/{USERNAME}/auth + */ @Serializable data class AccountPasswordChange( val new_password: String ) + +/** + * Request of PATCH /accounts/{USERNAME} + */ +@Serializable +data class AccountReconfiguration( + val challenge_contact_data: ChallengeContactData?, + val cashout_address: String?, + val name: String?, + val is_exchange: Boolean +) +\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt @@ -203,7 +203,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { } throw LibeufinBankException( httpStatus = HttpStatusCode.Conflict, talerError = TalerError( - code = GENERIC_UNDEFINED, // GANA needs this. + code = GENERIC_UNDEFINED, // FIXME: provide appropriate EC. hint = "Idempotency check failed." ) ) @@ -261,18 +261,16 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { } 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 resourceName = call.expectUriComponent("USERNAME") + if (!resourceName.canI(c, withAdmin = true)) throw forbidden() + val customerData = db.customerGetFromLogin(resourceName) ?: throw notFound( + "Customer '$resourceName' not found in the database.", + talerEc = TalerErrorCode.TALER_EC_END + ) + val bankAccountData = db.bankAccountGetFromOwnerId(customerData.expectRowId()) + ?: throw internalServerError("Customer '$resourceName' had no bank account despite they are customer.'") val balance = Balance( - amount = bankAccountData.balance ?: throw internalServerError("Account '${c.login}' lacks balance!"), + amount = bankAccountData.balance ?: throw internalServerError("Account '${customerData.login}' lacks balance!"), credit_debit_indicator = if (bankAccountData.hasDebt) { CorebankCreditDebitInfo.debit } else { @@ -329,17 +327,58 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { if (!accountName.canI(c, withAdmin = true)) throw forbidden() val req = call.receive<AccountPasswordChange>() val hashedPassword = CryptoUtil.hashpw(req.new_password) + /** + * FIXME: should it check if the password used to authenticate + * FIXME: this request _is_ the one being overridden in the database? + */ if (!db.customerChangePassword( accountName, hashedPassword )) throw notFound( - "Account '$accountName' not found", + "Account '$accountName' not found (despite it being authenticated by this call)", talerEc = TalerErrorCode.TALER_EC_END // FIXME: need at least GENERIC_NOT_FOUND. ) call.respond(HttpStatusCode.NoContent) return@patch } + patch("/accounts/{USERNAME}") { + val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: throw unauthorized() + val accountName = call.getResourceName("USERNAME") + // preventing user non-admin X trying on resource Y. + if (!accountName.canI(c, withAdmin = true)) throw forbidden() + // admin is not allowed itself to change its own details. + if (accountName == "admin") throw forbidden("admin account not patchable") + // authentication OK, go on. + val req = call.receive<AccountReconfiguration>() + /** + * This object holds the details of the customer that's affected + * by this operation, as it MAY differ from the one being authenticated. + * This typically happens when admin did the request. + */ + val accountCustomer = db.customerGetFromLogin(accountName) ?: throw notFound( + "Account $accountName not found", + talerEc = TalerErrorCode.TALER_EC_END // FIXME, define EC. + ) + // Check if a non-admin user tried to change their legal name + if (c.login != "admin" && req.name != accountCustomer.name) + throw forbidden("non-admin user cannot change their legal name") + // Preventing identical data to be overridden. + val bankAccount = db.bankAccountGetFromOwnerId(accountCustomer.expectRowId()) + ?: throw internalServerError("Customer '${accountCustomer.login}' lacks bank account.") + if ( + (req.is_exchange == bankAccount.isTalerExchange) && + (req.cashout_address == accountCustomer.cashoutPayto) && + (req.name == c.name) && + (req.challenge_contact_data?.phone == accountCustomer.phone) && + (req.challenge_contact_data?.email == accountCustomer.email) + ) { + call.respond(HttpStatusCode.NoContent) + return@patch + } + // Not identical, go on writing the DB. + throw NotImplementedError("DB part missing.") + } // WITHDRAWAL ENDPOINTS post("/accounts/{USERNAME}/withdrawals") { val c = call.authenticateBankRequest(db, TokenScope.readwrite) @@ -450,14 +489,19 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { // TRANSACTION ENDPOINT 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 resourceName = call.getResourceName("USERNAME") + if (!resourceName.canI(c, withAdmin = true)) throw forbidden() + val historyParams = getHistoryParams(call.request) + val resourceCustomer = db.customerGetFromLogin(resourceName) ?: throw notFound( + hint = "Customer '$resourceName' not found in the database", + talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC. + ) + val bankAccount = db.bankAccountGetFromOwnerId(resourceCustomer.expectRowId()) + ?: throw internalServerError("Customer '${resourceCustomer.login}' lacks bank account.") val history: List<BankAccountTransaction> = db.bankTransactionGetHistory( - start = historyParams.start, delta = historyParams.delta, bankAccountId = bankAccountId + start = historyParams.start, + delta = historyParams.delta, + bankAccountId = bankAccount.expectRowId() ) val res = BankAccountTransactionsResponse(transactions = mutableListOf()) history.forEach { @@ -526,8 +570,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { } 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 accountName = call.getResourceName("USERNAME") + if (!accountName.canI(c, withAdmin = true)) throw forbidden() val tId = call.expectUriComponent("T_ID") val txRowId = try { tId.toLong() @@ -535,14 +579,17 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { 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_BANK_TRANSACTION_NOT_FOUND ) - 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. + val accountCustomer = db.customerGetFromLogin(accountName) ?: throw notFound( + hint = "Customer $accountName not found", + talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC. + ) + val customerBankAccount = db.bankAccountGetFromOwnerId(accountCustomer.expectRowId()) + ?: throw internalServerError("Customer '${accountCustomer.login}' lacks bank account.") + if (tx.bankAccountId != customerBankAccount.bankAccountId) throw forbidden("Client has no rights over the bank transaction: $tId") call.respond( BankAccountTransactionInfo( amount = tx.amount, diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt @@ -43,17 +43,21 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") { val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized() - if (!call.getResourceName("USERNAME").canI(c, withAdmin = true)) throw forbidden() + val accountName = call.getResourceName("USERNAME") + if (!accountName.canI(c, withAdmin = true)) throw forbidden() val params = getHistoryParams(call.request) - val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) - ?: throw internalServerError("Customer '${c.login}' lacks bank account.") + val accountCustomer = db.customerGetFromLogin(accountName) ?: throw notFound( + hint = "Customer $accountName not found", + talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC. + ) + val bankAccount = db.bankAccountGetFromOwnerId(accountCustomer.expectRowId()) + ?: throw internalServerError("Customer '$accountName' lacks bank account.") if (!bankAccount.isTalerExchange) throw forbidden("History is not related to a Taler exchange.") - val bankAccountId = bankAccount.expectRowId() val history: List<BankAccountTransaction> = db.bankTransactionGetHistory( start = params.start, delta = params.delta, - bankAccountId = bankAccountId, + bankAccountId = bankAccount.expectRowId(), withDirection = TransactionDirection.credit ) if (history.isEmpty()) { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -164,7 +164,8 @@ fun internalServerError(hint: String?): LibeufinBankException = LibeufinBankExce fun notFound( - hint: String?, talerEc: TalerErrorCode + hint: String?, + talerEc: TalerErrorCode ): LibeufinBankException = LibeufinBankException( httpStatus = HttpStatusCode.NotFound, talerError = TalerError( code = talerEc.code, hint = hint