commit 05c52309ee2453836063c864241a5ec174a77786
parent b83ca72dfbc015ed3dd8a351cf812e1757632eab
Author: Antoine A <>
Date: Sat, 28 Oct 2023 01:44:28 +0000
Integrate auth in ktor pipeline
Diffstat:
7 files changed, 577 insertions(+), 586 deletions(-)
diff --git a/bank/build.gradle b/bank/build.gradle
@@ -48,7 +48,6 @@ dependencies {
implementation 'ch.qos.logback:logback-classic:1.4.5'
implementation project(":util")
-
// XML:
implementation "javax.xml.bind:jaxb-api:2.3.0"
implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1"
@@ -63,14 +62,10 @@ dependencies {
implementation "io.ktor:ktor-server-cors:$ktor_version"
implementation "io.ktor:ktor-server-content-negotiation:$ktor_version"
implementation "io.ktor:ktor-server-status-pages:$ktor_version"
- implementation "io.ktor:ktor-client-apache:$ktor_version"
- implementation "io.ktor:ktor-client-auth:$ktor_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-server-test-host:$ktor_version"
- implementation "io.ktor:ktor-auth:$ktor_auth_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0"
implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version"
- implementation "io.ktor:ktor-server-request-validation:$ktor_version"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
@@ -20,37 +20,67 @@ package tech.libeufin.bank
import io.ktor.http.*
import io.ktor.server.application.*
+import io.ktor.server.routing.Route
+import io.ktor.server.routing.RouteSelector
+import io.ktor.server.routing.RoutingResolveContext
+import io.ktor.server.routing.RouteSelectorEvaluation
+import io.ktor.util.AttributeKey
+import io.ktor.util.pipeline.PipelineContext
import net.taler.common.errorcodes.TalerErrorCode
import tech.libeufin.util.*
import net.taler.wallet.crypto.Base32Crockford
import java.time.Instant
-/** Authenticate admin */
-suspend fun ApplicationCall.authAdmin(db: Database, scope: TokenScope) {
- val login = authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login")
- if (login != "admin") {
- throw unauthorized("Only administrator allowed")
+private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin");
+
+/** Restrict route access to admin */
+fun Route.authAdmin(db: Database, scope: TokenScope, enforce: Boolean = true, callback: Route.() -> Unit): Route =
+ intercept(callback) {
+ if (enforce) {
+ val login = context.authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login")
+ if (login != "admin") {
+ throw unauthorized("Only administrator allowed")
+ }
+ }
}
-
-}
+
/** Authenticate and check access rights */
-suspend fun ApplicationCall.authCheck(db: Database, scope: TokenScope, withAdmin: Boolean = false, requireAdmin: Boolean = false): Pair<String, Boolean> {
- val authLogin = authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login")
- val login = accountLogin()
- if (requireAdmin && authLogin != "admin") {
- if (authLogin != "admin") {
- throw unauthorized("Only administrator allowed")
- }
- } else {
- val hasRight = authLogin == login || (withAdmin && authLogin == "admin");
- if (!hasRight) {
- throw unauthorized("Customer $authLogin have no right on $login account")
+fun Route.auth(db: Database, scope: TokenScope, allowAdmin: Boolean = false, requireAdmin: Boolean = false, callback: Route.() -> Unit): Route =
+ intercept(callback) {
+ val authLogin = context.authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login")
+ if (requireAdmin && authLogin != "admin") {
+ if (authLogin != "admin") {
+ throw unauthorized("Only administrator allowed")
+ }
+ } else {
+ println("$allowAdmin, $authLogin")
+ val hasRight = authLogin == username || (allowAdmin && authLogin == "admin");
+ if (!hasRight) {
+ throw unauthorized("Customer $authLogin have no right on $username account")
+ }
}
+ context.attributes.put(AUTH_IS_ADMIN, authLogin == "admin")
}
- return Pair(login, authLogin == "admin")
-}
+val PipelineContext<Unit, ApplicationCall>.username: String get() = call.username
+val PipelineContext<Unit, ApplicationCall>.isAdmin: Boolean get() = call.isAdmin
+val ApplicationCall.username: String get() = expectUriComponent("USERNAME")
+val ApplicationCall.isAdmin: Boolean get() = attributes.getOrNull(AUTH_IS_ADMIN) ?: throw Exception("No auth")
+
+private fun Route.intercept(callback: Route.() -> Unit, interceptor: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit): Route {
+ val subRoute = createChild(object : RouteSelector() {
+ override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation =
+ RouteSelectorEvaluation.Constant
+ })
+ subRoute.intercept(ApplicationCallPipeline.Plugins) {
+ interceptor()
+ proceed()
+ }
+
+ callback(subRoute)
+ return subRoute
+}
/**
* This function tries to authenticate the call according
* to the scheme that is mentioned in the Authorization header.
@@ -73,7 +103,7 @@ private suspend fun ApplicationCall.authenticateBankRequest(db: Database, requir
return when (authDetails.scheme) {
"Basic" -> doBasicAuth(db, authDetails.content)
"Bearer" -> doTokenAuth(db, authDetails.content, requiredScope)
- else -> throw unauthorized("Authorization method wrong or not supported.")
+ else -> throw unauthorized("Authorization method wrong or not supported.") // TODO basic auth challenge
}
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -48,10 +48,11 @@ fun Routing.coreBankApi(db: Database, ctx: BankConfig) {
)
)
}
- get("/monitor") {
- call.authAdmin(db, TokenScope.readonly)
- val params = MonitorParams.extract(call.request.queryParameters)
- call.respond(db.monitor(params))
+ authAdmin(db, TokenScope.readonly) {
+ get("/monitor") {
+ val params = MonitorParams.extract(call.request.queryParameters)
+ call.respond(db.monitor(params))
+ }
}
coreBankTokenApi(db)
coreBankAccountsMgmtApi(db, ctx)
@@ -61,191 +62,189 @@ fun Routing.coreBankApi(db: Database, ctx: BankConfig) {
}
private fun Routing.coreBankTokenApi(db: Database) {
- post("/accounts/{USERNAME}/token") {
- val (login, _) = call.authCheck(db, TokenScope.refreshable)
- 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 tokenDuration: Duration = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION
+ auth(db, TokenScope.refreshable) {
+ post("/accounts/{USERNAME}/token") {
+ 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 tokenDuration: Duration = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION
- val creationTime = Instant.now()
- val expirationTimestamp =
- if (tokenDuration == ChronoUnit.FOREVER.duration) {
- logger.debug("Creating 'forever' token.")
- Instant.MAX
- } else {
- try {
- logger.debug("Creating token with days duration: ${tokenDuration.toDays()}")
- creationTime.plus(tokenDuration)
- } catch (e: Exception) {
- logger.error("Could not add token duration to current time: ${e.message}")
- throw badRequest("Bad token duration: ${e.message}")
+ val creationTime = Instant.now()
+ val expirationTimestamp =
+ if (tokenDuration == ChronoUnit.FOREVER.duration) {
+ logger.debug("Creating 'forever' token.")
+ Instant.MAX
+ } else {
+ try {
+ logger.debug("Creating token with days duration: ${tokenDuration.toDays()}")
+ creationTime.plus(tokenDuration)
+ } catch (e: Exception) {
+ logger.error("Could not add token duration to current time: ${e.message}")
+ throw badRequest("Bad token duration: ${e.message}")
+ }
}
- }
- if (!db.bearerTokenCreate(
- login = login,
- content = tokenBytes,
- creationTime = creationTime,
- expirationTime = expirationTimestamp,
- scope = req.scope,
- isRefreshable = req.refreshable
- )) {
- throw internalServerError("Failed at inserting new token in the database")
- }
- call.respond(
- TokenSuccessResponse(
- access_token = Base32Crockford.encode(tokenBytes),
- expiration = TalerProtocolTimestamp(t_s = expirationTimestamp)
+ if (!db.bearerTokenCreate(
+ login = username,
+ content = tokenBytes,
+ creationTime = creationTime,
+ expirationTime = expirationTimestamp,
+ scope = req.scope,
+ isRefreshable = req.refreshable
+ )) {
+ throw internalServerError("Failed at inserting new token in the database")
+ }
+ call.respond(
+ TokenSuccessResponse(
+ access_token = Base32Crockford.encode(tokenBytes),
+ expiration = TalerProtocolTimestamp(t_s = expirationTimestamp)
+ )
)
- )
+ }
}
- delete("/accounts/{USERNAME}/token") {
- call.authCheck(db, TokenScope.readonly)
- val token = call.getAuthToken() ?: throw badRequest("Basic auth not supported here.")
+ auth(db, TokenScope.readonly) {
+ delete("/accounts/{USERNAME}/token") {
+ val token = call.getAuthToken() ?: throw badRequest("Basic auth not supported here.")
- /**
- * Not sanity-checking the token, as it was used by the authentication already. If harder
- * errors happen, then they'll get Ktor respond with 500.
- */
- db.bearerTokenDelete(Base32Crockford.decode(token))
- /**
- * Responding 204 regardless of it being actually deleted or not. If it wasn't found, then
- * it must have been deleted before we reached here, but the token was valid as it served
- * the authentication => no reason to fail the request.
- */
- call.respond(HttpStatusCode.NoContent)
+ /**
+ * Not sanity-checking the token, as it was used by the authentication already. If harder
+ * errors happen, then they'll get Ktor respond with 500.
+ */
+ db.bearerTokenDelete(Base32Crockford.decode(token))
+ /**
+ * Responding 204 regardless of it being actually deleted or not. If it wasn't found, then
+ * it must have been deleted before we reached here, but the token was valid as it served
+ * the authentication => no reason to fail the request.
+ */
+ call.respond(HttpStatusCode.NoContent)
+ }
}
}
private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) {
- post("/accounts") {
- // check if only admin is allowed to create new accounts
- if (ctx.restrictRegistration) {
- call.authAdmin(db, TokenScope.readwrite)
- } // auth passed, proceed with activity.
- val req = call.receive<RegisterAccountRequest>()
- // Prohibit reserved usernames:
- if (reservedAccounts.contains(req.username))
- throw forbidden(
- "Username '${req.username}' is reserved.",
- TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
- )
-
- val internalPayto = req.internal_payto_uri ?: IbanPayTo(genIbanPaytoUri())
- val result = db.accountCreate(
- login = req.username,
- name = req.name,
- email = req.challenge_contact_data?.email,
- phone = req.challenge_contact_data?.phone,
- cashoutPayto = req.cashout_payto_uri,
- password = req.password,
- internalPaytoUri = internalPayto,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = ctx.defaultCustomerDebtLimit,
- bonus = if (ctx.registrationBonusEnabled && !req.is_taler_exchange) ctx.registrationBonus
- else null
- )
+ authAdmin(db, TokenScope.readwrite, ctx.restrictRegistration) {
+ post("/accounts") {
+ val req = call.receive<RegisterAccountRequest>()
+ // Prohibit reserved usernames:
+ if (reservedAccounts.contains(req.username))
+ throw forbidden(
+ "Username '${req.username}' is reserved.",
+ TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
+ )
- when (result) {
- CustomerCreationResult.BALANCE_INSUFFICIENT -> throw conflict(
- "Insufficient funds",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ val internalPayto = req.internal_payto_uri ?: IbanPayTo(genIbanPaytoUri())
+ val result = db.accountCreate(
+ login = req.username,
+ name = req.name,
+ email = req.challenge_contact_data?.email,
+ phone = req.challenge_contact_data?.phone,
+ cashoutPayto = req.cashout_payto_uri,
+ password = req.password,
+ internalPaytoUri = internalPayto,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = ctx.defaultCustomerDebtLimit,
+ bonus = if (ctx.registrationBonusEnabled && !req.is_taler_exchange) ctx.registrationBonus
+ else null
)
- CustomerCreationResult.CONFLICT_LOGIN -> throw conflict(
- "Customer username reuse '${req.username}'",
- TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
- )
- CustomerCreationResult.CONFLICT_PAY_TO -> throw conflict(
- "Bank internalPayToUri reuse '${internalPayto.canonical}'",
- TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
- )
- CustomerCreationResult.SUCCESS -> call.respond(HttpStatusCode.Created)
+
+ when (result) {
+ CustomerCreationResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ CustomerCreationResult.CONFLICT_LOGIN -> throw conflict(
+ "Customer username reuse '${req.username}'",
+ TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
+ )
+ CustomerCreationResult.CONFLICT_PAY_TO -> throw conflict(
+ "Bank internalPayToUri reuse '${internalPayto.canonical}'",
+ TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
+ )
+ CustomerCreationResult.SUCCESS -> call.respond(HttpStatusCode.Created)
+ }
}
}
- delete("/accounts/{USERNAME}") {
- val (login, _) =
- call.authCheck(
- db,
- TokenScope.readwrite,
- withAdmin = true,
- requireAdmin = ctx.restrictAccountDeletion
- )
- // Not deleting reserved names.
- if (reservedAccounts.contains(login))
+ auth(
+ db,
+ TokenScope.readwrite,
+ allowAdmin = true,
+ requireAdmin = ctx.restrictAccountDeletion
+ ) {
+ delete("/accounts/{USERNAME}") {
+ // Not deleting reserved names.
+ if (reservedAccounts.contains(username))
throw forbidden(
- "Cannot delete reserved accounts",
- TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
+ "Cannot delete reserved accounts",
+ TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
)
- when (db.customerDeleteIfBalanceIsZero(login)) {
- CustomerDeletionResult.CUSTOMER_NOT_FOUND ->
- throw notFound(
- "Customer '$login' not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- CustomerDeletionResult.BALANCE_NOT_ZERO ->
- throw conflict(
- "Balance is not zero.",
- TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
- )
- CustomerDeletionResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ when (db.customerDeleteIfBalanceIsZero(username)) {
+ CustomerDeletionResult.CUSTOMER_NOT_FOUND -> throw notFound(
+ "Customer '$username' not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ CustomerDeletionResult.BALANCE_NOT_ZERO -> throw conflict(
+ "Balance is not zero.",
+ TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
+ )
+ CustomerDeletionResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ }
}
}
- patch("/accounts/{USERNAME}") {
- val (login, isAdmin) = call.authCheck(db, TokenScope.readwrite, withAdmin = true)
- // admin is not allowed itself to change its own details.
- if (login == "admin") throw forbidden("admin account not patchable")
-
- val req = call.receive<AccountReconfiguration>()
- val res = db.accountReconfig(
- login = login,
- name = req.name,
- cashoutPayto = req.cashout_address,
- emailAddress = req.challenge_contact_data?.email,
- isTalerExchange = req.is_exchange,
- phoneNumber = req.challenge_contact_data?.phone,
- isAdmin = isAdmin
- )
- when (res) {
- CustomerPatchResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
- CustomerPatchResult.ACCOUNT_NOT_FOUND -> throw notFound(
- "Customer '$login' not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ auth(db, TokenScope.readwrite, allowAdmin = true) {
+ patch("/accounts/{USERNAME}") {
+ // admin is not allowed itself to change its own details.
+ if (username == "admin") throw forbidden("admin account not patchable")
+
+ val req = call.receive<AccountReconfiguration>()
+ val res = db.accountReconfig(
+ login = username,
+ name = req.name,
+ cashoutPayto = req.cashout_address,
+ emailAddress = req.challenge_contact_data?.email,
+ isTalerExchange = req.is_exchange,
+ phoneNumber = req.challenge_contact_data?.phone,
+ isAdmin = isAdmin
)
- CustomerPatchResult.CONFLICT_LEGAL_NAME ->
- throw forbidden("non-admin user cannot change their legal name")
+ when (res) {
+ CustomerPatchResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ CustomerPatchResult.ACCOUNT_NOT_FOUND -> throw notFound(
+ "Customer '$username' not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ CustomerPatchResult.CONFLICT_LEGAL_NAME ->
+ throw forbidden("non-admin user cannot change their legal name")
+ }
}
}
- patch("/accounts/{USERNAME}/auth") {
- val (login, _) = call.authCheck(db, TokenScope.readwrite)
- val req = call.receive<AccountPasswordChange>()
- val hashedPassword = CryptoUtil.hashpw(req.new_password)
- if (!db.customerChangePassword(login, hashedPassword))
+ auth(db, TokenScope.readwrite) {
+ patch("/accounts/{USERNAME}/auth") {
+ val req = call.receive<AccountPasswordChange>()
+ val hashedPassword = CryptoUtil.hashpw(req.new_password)
+ if (!db.customerChangePassword(username, hashedPassword))
throw notFound(
- "Account '$login' not found (despite it being authenticated by this call)",
- talerEc =
- TalerErrorCode
- .TALER_EC_END // FIXME: need at least GENERIC_NOT_FOUND.
+ "Account '$username' not found (despite it being authenticated by this call)",
+ TalerErrorCode.TALER_EC_END // FIXME: need at least GENERIC_NOT_FOUND.
)
- call.respond(HttpStatusCode.NoContent)
+ call.respond(HttpStatusCode.NoContent)
+ }
}
get("/public-accounts") {
// no authentication here.
@@ -256,166 +255,159 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) {
call.respond(PublicAccountsResponse(publicAccounts))
}
}
- get("/accounts") {
- call.authAdmin(db, TokenScope.readonly)
- // Get optional param.
- val maybeFilter: String? = call.request.queryParameters["filter_name"]
- logger.debug("Filtering on '${maybeFilter}'")
- val queryParam =
- if (maybeFilter != null) {
- "%${maybeFilter}%"
- } else "%"
- val accounts = db.accountsGetForAdmin(queryParam)
- if (accounts.isEmpty()) {
- call.respond(HttpStatusCode.NoContent)
- } else {
- call.respond(ListBankAccountsResponse(accounts))
+ authAdmin(db, TokenScope.readonly) {
+ get("/accounts") {
+ // Get optional param.
+ val maybeFilter: String? = call.request.queryParameters["filter_name"]
+ logger.debug("Filtering on '${maybeFilter}'")
+ val queryParam =
+ if (maybeFilter != null) {
+ "%${maybeFilter}%"
+ } else "%"
+ val accounts = db.accountsGetForAdmin(queryParam)
+ if (accounts.isEmpty()) {
+ call.respond(HttpStatusCode.NoContent)
+ } else {
+ call.respond(ListBankAccountsResponse(accounts))
+ }
}
}
- get("/accounts/{USERNAME}") {
- val (login, _) = call.authCheck(db, TokenScope.readonly, withAdmin = true)
- val account = db.accountDataFromLogin(login) ?: throw notFound(
- "Customer '$login' not found in the database.",
- talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- call.respond(account)
+ auth(db, TokenScope.readonly, allowAdmin = true) {
+ get("/accounts/{USERNAME}") {
+ val account = db.accountDataFromLogin(username) ?: throw notFound(
+ "Customer '$username' not found in the database.",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ call.respond(account)
+ }
}
}
private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) {
- get("/accounts/{USERNAME}/transactions") {
- call.authCheck(db, TokenScope.readonly)
- val params = HistoryParams.extract(call.request.queryParameters)
- val bankAccount = call.bankAccount(db)
-
- val history: List<BankAccountTransactionInfo> =
- db.bankPoolHistory(params, bankAccount.bankAccountId)
- call.respond(BankAccountTransactionsResponse(history))
- }
- get("/accounts/{USERNAME}/transactions/{T_ID}") {
- call.authCheck(db, TokenScope.readonly)
- 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 bankAccount = call.bankAccount(db)
- val tx =
- db.bankTransactionGetFromInternalId(txRowId)
- ?: throw notFound(
- "Bank transaction '$tId' not found",
- TalerErrorCode.TALER_EC_BANK_TRANSACTION_NOT_FOUND
- )
- if (tx.bankAccountId != bankAccount.bankAccountId) // TODO not found ?
- throw unauthorized("Client has no rights over the bank transaction: $tId")
-
- call.respond(
+ auth(db, TokenScope.readonly) {
+ get("/accounts/{USERNAME}/transactions") {
+ val params = HistoryParams.extract(call.request.queryParameters)
+ val bankAccount = call.bankAccount(db)
+
+ val history: List<BankAccountTransactionInfo> =
+ db.bankPoolHistory(params, bankAccount.bankAccountId)
+ call.respond(BankAccountTransactionsResponse(history))
+ }
+ get("/accounts/{USERNAME}/transactions/{T_ID}") {
+ 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 bankAccount = call.bankAccount(db)
+ val tx =
+ db.bankTransactionGetFromInternalId(txRowId)
+ ?: throw notFound(
+ "Bank transaction '$tId' not found",
+ TalerErrorCode.TALER_EC_BANK_TRANSACTION_NOT_FOUND
+ )
+ if (tx.bankAccountId != bankAccount.bankAccountId) // TODO not found ?
+ throw unauthorized("Client has no rights over the bank transaction: $tId")
+
+ call.respond(
BankAccountTransactionInfo(
- amount = tx.amount,
- creditor_payto_uri = tx.creditorPaytoUri,
- debtor_payto_uri = tx.debtorPaytoUri,
- date = TalerProtocolTimestamp(tx.transactionDate),
- direction = tx.direction,
- subject = tx.subject,
- row_id = txRowId
+ amount = tx.amount,
+ creditor_payto_uri = tx.creditorPaytoUri,
+ debtor_payto_uri = tx.debtorPaytoUri,
+ date = TalerProtocolTimestamp(tx.transactionDate),
+ direction = tx.direction,
+ subject = tx.subject,
+ row_id = txRowId
)
- )
+ )
+ }
}
- post("/accounts/{USERNAME}/transactions") {
- val (login, _) = call.authCheck(db, TokenScope.readwrite)
- val tx = call.receive<BankAccountTransactionCreate>()
-
- val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer lacks subject")
- val amount =
- tx.payto_uri.amount ?: tx.amount ?: throw badRequest("Wire transfer lacks amount")
- ctx.checkInternalCurrency(amount)
- val result =
- db.bankTransaction(
- creditAccountPayto = tx.payto_uri,
- debitAccountUsername = login,
- subject = subject,
- amount = amount,
- timestamp = Instant.now(),
+ auth(db, TokenScope.readwrite) {
+ post("/accounts/{USERNAME}/transactions") {
+ val tx = call.receive<BankAccountTransactionCreate>()
+ val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer lacks subject")
+ val amount =
+ tx.payto_uri.amount ?: tx.amount ?: throw badRequest("Wire transfer lacks amount")
+ ctx.checkInternalCurrency(amount)
+ val result = db.bankTransaction(
+ creditAccountPayto = tx.payto_uri,
+ debitAccountUsername = username,
+ subject = subject,
+ amount = amount,
+ timestamp = Instant.now(),
+ )
+ when (result) {
+ BankTransactionResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
)
- when (result) {
- BankTransactionResult.BALANCE_INSUFFICIENT ->
- throw conflict(
- "Insufficient funds",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
- )
- BankTransactionResult.SAME_ACCOUNT ->
- throw conflict(
- "Wire transfer attempted with credit and debit party being the same bank account",
- TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
- )
- BankTransactionResult.NO_DEBTOR ->
- throw notFound(
- "Customer $login not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- BankTransactionResult.NO_CREDITOR ->
- throw notFound(
- "Creditor account was not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ BankTransactionResult.SAME_ACCOUNT -> throw conflict(
+ "Wire transfer attempted with credit and debit party being the same bank account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ BankTransactionResult.NO_DEBTOR -> throw notFound(
+ "Customer $username not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ BankTransactionResult.NO_CREDITOR -> throw notFound(
+ "Creditor account was not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ }
}
}
}
fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) {
- post("/accounts/{USERNAME}/withdrawals") {
- val (login, _) = call.authCheck(db, TokenScope.readwrite)
- val req = call.receive<BankAccountCreateWithdrawalRequest>()
-
- ctx.checkInternalCurrency(req.amount)
-
- val opId = UUID.randomUUID()
- when (db.talerWithdrawalCreate(login, opId, req.amount)) {
- WithdrawalCreationResult.ACCOUNT_NOT_FOUND ->
- throw notFound(
- "Customer $login not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- WithdrawalCreationResult.ACCOUNT_IS_EXCHANGE ->
- throw conflict(
- "Exchange account cannot perform withdrawal operation",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- WithdrawalCreationResult.BALANCE_INSUFFICIENT ->
- throw conflict(
- "Insufficient funds to withdraw with Taler",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
- )
- WithdrawalCreationResult.SUCCESS -> {
- 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())
- )
+ auth(db, TokenScope.readwrite) {
+ post("/accounts/{USERNAME}/withdrawals") {
+ val req = call.receive<BankAccountCreateWithdrawalRequest>()
+ ctx.checkInternalCurrency(req.amount)
+ val opId = UUID.randomUUID()
+ when (db.talerWithdrawalCreate(username, opId, req.amount)) {
+ WithdrawalCreationResult.ACCOUNT_NOT_FOUND -> throw notFound(
+ "Customer $username not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ WithdrawalCreationResult.ACCOUNT_IS_EXCHANGE -> throw conflict(
+ "Exchange account cannot perform withdrawal operation",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
)
+ WithdrawalCreationResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds to withdraw with Taler",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ WithdrawalCreationResult.SUCCESS -> {
+ 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())
+ )
+ )
+ }
}
}
}
get("/withdrawals/{withdrawal_id}") {
val op = call.getWithdrawal(db, "withdrawal_id")
call.respond(
- BankAccountGetWithdrawalResponse(
- amount = op.amount,
- aborted = op.aborted,
- confirmation_done = op.confirmationDone,
- selection_done = op.selectionDone,
- selected_exchange_account = op.selectedExchangePayto,
- selected_reserve_pub = op.reservePub
- )
+ BankAccountGetWithdrawalResponse(
+ amount = op.amount,
+ aborted = op.aborted,
+ confirmation_done = op.confirmationDone,
+ selection_done = op.selectionDone,
+ selected_exchange_account = op.selectedExchangePayto,
+ selected_reserve_pub = op.reservePub
+ )
)
}
post("/withdrawals/{withdrawal_id}/abort") {
@@ -433,16 +425,14 @@ fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) {
post("/withdrawals/{withdrawal_id}/confirm") {
val opId = call.uuidUriComponent("withdrawal_id")
when (db.talerWithdrawalConfirm(opId, Instant.now())) {
- WithdrawalConfirmationResult.OP_NOT_FOUND ->
- throw notFound(
- "Withdrawal operation $opId not found",
- TalerErrorCode.TALER_EC_END
- )
- WithdrawalConfirmationResult.ABORTED ->
- throw conflict(
- "Cannot confirm an aborted withdrawal",
- TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
- )
+ WithdrawalConfirmationResult.OP_NOT_FOUND -> throw notFound(
+ "Withdrawal operation $opId not found",
+ TalerErrorCode.TALER_EC_END
+ )
+ WithdrawalConfirmationResult.ABORTED -> throw conflict(
+ "Cannot confirm an aborted withdrawal",
+ TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+ )
WithdrawalConfirmationResult.NOT_SELECTED ->
throw LibeufinBankException(
httpStatus = HttpStatusCode.UnprocessableEntity,
@@ -452,129 +442,127 @@ fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) {
code = TalerErrorCode.TALER_EC_END.code
)
)
- WithdrawalConfirmationResult.BALANCE_INSUFFICIENT ->
- throw conflict(
- "Insufficient funds",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
- )
- WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND ->
- throw conflict(
- "Exchange to withdraw from not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
+ WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND -> throw conflict(
+ "Exchange to withdraw from not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
WithdrawalConfirmationResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
}
}
}
fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) {
- post("/accounts/{USERNAME}/cashouts") {
- val (login, _) = call.authCheck(db, TokenScope.readwrite)
- val req = call.receive<CashoutRequest>()
+ auth(db, TokenScope.readwrite) {
+ post("/accounts/{USERNAME}/cashouts") {
+ val req = call.receive<CashoutRequest>()
- ctx.checkInternalCurrency(req.amount_debit)
- ctx.checkFiatCurrency(req.amount_credit)
+ ctx.checkInternalCurrency(req.amount_debit)
+ ctx.checkFiatCurrency(req.amount_credit)
- val opId = UUID.randomUUID()
- val tanChannel = req.tan_channel ?: TanChannel.sms
- val tanCode = UUID.randomUUID().toString()
- val (status, info) = db.cashoutCreate(
- accountUsername = login,
- cashoutUuid = opId,
- amountDebit = req.amount_debit,
- amountCredit = req.amount_credit,
- subject = req.subject ?: "", // TODO default subject
- creationTime = Instant.now(),
- tanChannel = tanChannel,
- tanCode = tanCode
- )
- when (status) {
- CashoutCreationResult.BAD_CONVERSION -> throw conflict(
- "Wrong currency conversion",
- TalerErrorCode.TALER_EC_END // TODO EC ?
- )
- CashoutCreationResult.ACCOUNT_NOT_FOUND -> throw notFound(
- "Customer $login not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- CashoutCreationResult.ACCOUNT_IS_EXCHANGE -> throw conflict(
- "Exchange account cannot perform cashout operation",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ val opId = UUID.randomUUID()
+ val tanChannel = req.tan_channel ?: TanChannel.sms
+ val tanCode = UUID.randomUUID().toString()
+ val (status, info) = db.cashoutCreate(
+ accountUsername = username,
+ cashoutUuid = opId,
+ amountDebit = req.amount_debit,
+ amountCredit = req.amount_credit,
+ subject = req.subject ?: "", // TODO default subject
+ creationTime = Instant.now(),
+ tanChannel = tanChannel,
+ tanCode = tanCode
)
- CashoutCreationResult.BALANCE_INSUFFICIENT -> throw conflict(
- "Insufficient funds to withdraw with Taler",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
- )
- CashoutCreationResult.MISSING_TAN_INFO -> throw conflict(
- "Customer $login missing iinfo for tan channel ${req.tan_channel}",
- TalerErrorCode.TALER_EC_END // TODO EC ?
- )
- CashoutCreationResult.SUCCESS -> {
- when (tanChannel) {
- TanChannel.sms -> throw Exception("TODO")
- TanChannel.email -> throw Exception("TODO")
- TanChannel.file -> {
- File("/tmp/cashout-tan.txt").writeText(tanCode)
+ when (status) {
+ CashoutCreationResult.BAD_CONVERSION -> throw conflict(
+ "Wrong currency conversion",
+ TalerErrorCode.TALER_EC_END // TODO EC ?
+ )
+ CashoutCreationResult.ACCOUNT_NOT_FOUND -> throw notFound(
+ "Customer $username not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ CashoutCreationResult.ACCOUNT_IS_EXCHANGE -> throw conflict(
+ "Exchange account cannot perform cashout operation",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ CashoutCreationResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds to withdraw with Taler",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ CashoutCreationResult.MISSING_TAN_INFO -> throw conflict(
+ "Customer $username missing iinfo for tan channel ${req.tan_channel}",
+ TalerErrorCode.TALER_EC_END // TODO EC ?
+ )
+ CashoutCreationResult.SUCCESS -> {
+ when (tanChannel) {
+ TanChannel.sms -> throw Exception("TODO")
+ TanChannel.email -> throw Exception("TODO")
+ TanChannel.file -> {
+ File("/tmp/cashout-tan.txt").writeText(tanCode)
+ }
}
+ // TODO delete on error or commit transaction on error
+ call.respond(CashoutPending(opId.toString()))
}
- // TODO delete on error or commit transaction on error
- call.respond(CashoutPending(opId.toString()))
}
}
- }
- post("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}/abort") {
- call.authCheck(db, TokenScope.readwrite)
- val opId = call.uuidUriComponent("CASHOUT_ID")
- when (db.cashoutAbort(opId)) {
- AbortResult.NOT_FOUND -> throw notFound(
- "Cashout operation $opId not found",
- TalerErrorCode.TALER_EC_END
- )
- AbortResult.CONFIRMED ->
- throw conflict("Cannot abort confirmed cashout", TalerErrorCode.TALER_EC_END)
- AbortResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
- }
- }
- post("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}/confirm") {
- call.authCheck(db, TokenScope.readwrite)
- val req = call.receive<CashoutConfirm>()
- val opId = call.uuidUriComponent("CASHOUT_ID")
- when (db.cashoutConfirm(
- opUuid = opId,
- tanCode = req.tan,
- timestamp = Instant.now()
- )) {
- CashoutConfirmationResult.OP_NOT_FOUND -> throw notFound(
- "Cashout operation $opId not found",
- TalerErrorCode.TALER_EC_END
- )
- CashoutConfirmationResult.ABORTED -> throw conflict(
- "Cannot confirm an aborted cashout",
- TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
- )
- CashoutConfirmationResult.BAD_TAN_CODE -> throw forbidden(
- "Incorrect TAN code",
- TalerErrorCode.TALER_EC_END
- )
- CashoutConfirmationResult.BALANCE_INSUFFICIENT -> throw conflict(
- "Insufficient funds",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
- )
- CashoutConfirmationResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ post("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}/abort") {
+ val opId = call.uuidUriComponent("CASHOUT_ID")
+ when (db.cashoutAbort(opId)) {
+ AbortResult.NOT_FOUND -> throw notFound(
+ "Cashout operation $opId not found",
+ TalerErrorCode.TALER_EC_END
+ )
+ AbortResult.CONFIRMED ->
+ throw conflict("Cannot abort confirmed cashout", TalerErrorCode.TALER_EC_END)
+ AbortResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ }
}
+ post("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}/confirm") {
+ val req = call.receive<CashoutConfirm>()
+ val opId = call.uuidUriComponent("CASHOUT_ID")
+ when (db.cashoutConfirm(
+ opUuid = opId,
+ tanCode = req.tan,
+ timestamp = Instant.now()
+ )) {
+ CashoutConfirmationResult.OP_NOT_FOUND -> throw notFound(
+ "Cashout operation $opId not found",
+ TalerErrorCode.TALER_EC_END
+ )
+ CashoutConfirmationResult.ABORTED -> throw conflict(
+ "Cannot confirm an aborted cashout",
+ TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+ )
+ CashoutConfirmationResult.BAD_TAN_CODE -> throw forbidden(
+ "Incorrect TAN code",
+ TalerErrorCode.TALER_EC_END
+ )
+ CashoutConfirmationResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient funds",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ CashoutConfirmationResult.SUCCESS -> call.respond(HttpStatusCode.NoContent)
+ }
+ }
}
- get("/accounts/{USERNAME}/cashouts") {
- val (login, _) = call.authCheck(db, TokenScope.readonly)
- // TODO
- }
- get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}") {
- val (login, _) = call.authCheck(db, TokenScope.readonly)
- // TODO
+ auth(db, TokenScope.readonly) {
+ get("/accounts/{USERNAME}/cashouts") {
+ // TODO
+ }
+ get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}") {
+ // TODO
+ }
}
- get("/cashouts") {
- call.authAdmin(db, TokenScope.readonly)
- // TODO
+ authAdmin(db, TokenScope.readonly) {
+ get("/cashouts") {
+ // TODO
+ }
}
get("/cashout-rate") {
val params = CashoutRateParams.extract(call.request.queryParameters)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -36,7 +36,6 @@ import io.ktor.server.plugins.*
import io.ktor.server.plugins.callloging.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.plugins.cors.routing.*
-import io.ktor.server.plugins.requestvalidation.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.request.*
import io.ktor.server.response.*
@@ -129,7 +128,6 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) {
ignoreUnknownKeys = true
})
}
- install(RequestValidation)
install(StatusPages) {
/**
* This branch triggers when the Ktor layers detect one
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
@@ -26,6 +26,7 @@ import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
+import io.ktor.util.pipeline.PipelineContext
import net.taler.common.errorcodes.TalerErrorCode
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@@ -41,131 +42,128 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) {
call.respond(TWGConfigResponse(currency = ctx.currency))
return@get
}
-
- post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
- val (login, _) = call.authCheck(db, TokenScope.readwrite)
- val req = call.receive<TransferRequest>()
- ctx.checkInternalCurrency(req.amount)
- val dbRes = db.talerTransferCreate(
- req = req,
- username = login,
- timestamp = Instant.now()
- )
- when (dbRes.txResult) {
- TalerTransferResult.NO_DEBITOR -> throw notFound(
- "Customer $login not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- TalerTransferResult.NOT_EXCHANGE -> throw conflict(
- "$login is not an exchange account.",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- TalerTransferResult.NO_CREDITOR -> throw notFound(
- "Creditor account was not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- TalerTransferResult.SAME_ACCOUNT -> throw conflict(
- "Wire transfer attempted with credit and debit party being the same bank account",
- TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
- )
- TalerTransferResult.BOTH_EXCHANGE -> throw conflict(
- "Wire transfer attempted with credit and debit party being both exchange account",
- TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
- )
- TalerTransferResult.REQUEST_UID_REUSE -> throw conflict(
- "request_uid used already",
- TalerErrorCode.TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED
- )
- TalerTransferResult.BALANCE_INSUFFICIENT -> throw conflict(
- "Insufficient balance for exchange",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
- )
- TalerTransferResult.SUCCESS -> call.respond(
- TransferResponse(
- timestamp = dbRes.timestamp!!,
- row_id = dbRes.txRowId!!
+ auth(db, TokenScope.readwrite) {
+ post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
+ val req = call.receive<TransferRequest>()
+ ctx.checkInternalCurrency(req.amount)
+ val dbRes = db.talerTransferCreate(
+ req = req,
+ username = username,
+ timestamp = Instant.now()
+ )
+ when (dbRes.txResult) {
+ TalerTransferResult.NO_DEBITOR -> throw notFound(
+ "Customer $username not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
)
- )
+ TalerTransferResult.NOT_EXCHANGE -> throw conflict(
+ "$username is not an exchange account.",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ TalerTransferResult.NO_CREDITOR -> throw notFound(
+ "Creditor account was not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ TalerTransferResult.SAME_ACCOUNT -> throw conflict(
+ "Wire transfer attempted with credit and debit party being the same bank account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ TalerTransferResult.BOTH_EXCHANGE -> throw conflict(
+ "Wire transfer attempted with credit and debit party being both exchange account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ TalerTransferResult.REQUEST_UID_REUSE -> throw conflict(
+ "request_uid used already",
+ TalerErrorCode.TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED
+ )
+ TalerTransferResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient balance for exchange",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ TalerTransferResult.SUCCESS -> call.respond(
+ TransferResponse(
+ timestamp = dbRes.timestamp!!,
+ row_id = dbRes.txRowId!!
+ )
+ )
+ }
}
}
+ auth(db, TokenScope.readonly) {
+ suspend fun <T> PipelineContext<Unit, ApplicationCall>.historyEndpoint(
+ reduce: (List<T>, String) -> Any,
+ dbLambda: suspend Database.(HistoryParams, Long) -> List<T>
+ ) {
+ val params = HistoryParams.extract(context.request.queryParameters)
+ val bankAccount = call.bankAccount(db)
+
+ if (!bankAccount.isTalerExchange)
+ throw conflict(
+ "$username is not an exchange account.",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
- suspend fun <T> historyEndpoint(
- call: ApplicationCall,
- reduce: (List<T>, String) -> Any,
- dbLambda: suspend Database.(HistoryParams, Long) -> List<T>
- ) {
- val (login, _) = call.authCheck(db, TokenScope.readonly)
- val params = HistoryParams.extract(call.request.queryParameters)
- val bankAccount = call.bankAccount(db)
+ val items = db.dbLambda(params, bankAccount.bankAccountId);
- if (!bankAccount.isTalerExchange)
- throw conflict(
- "$login is not an exchange account.",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
-
- val items = db.dbLambda(params, bankAccount.bankAccountId!!);
-
- if (items.isEmpty()) {
- call.respond(HttpStatusCode.NoContent)
- } else {
- call.respond(reduce(items, bankAccount.internalPaytoUri.canonical))
+ if (items.isEmpty()) {
+ call.respond(HttpStatusCode.NoContent)
+ } else {
+ call.respond(reduce(items, bankAccount.internalPaytoUri.canonical))
+ }
+ }
+ get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
+ historyEndpoint(::IncomingHistory, Database::exchangeIncomingPoolHistory)
+ }
+ get("/accounts/{USERNAME}/taler-wire-gateway/history/outgoing") {
+ historyEndpoint(::OutgoingHistory, Database::exchangeOutgoingPoolHistory)
}
}
-
- get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
- historyEndpoint(call, ::IncomingHistory, Database::exchangeIncomingPoolHistory)
- }
-
- get("/accounts/{USERNAME}/taler-wire-gateway/history/outgoing") {
- historyEndpoint(call, ::OutgoingHistory, Database::exchangeOutgoingPoolHistory)
- }
-
- post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
- val (login, _) = call.authCheck(db, TokenScope.readwrite) // TODO authAdmin ?
- val req = call.receive<AddIncomingRequest>()
- ctx.checkInternalCurrency(req.amount)
- val timestamp = Instant.now()
- val dbRes = db.talerAddIncomingCreate(
- req = req,
- username = login,
- timestamp = timestamp
- )
- when (dbRes.txResult) {
- TalerAddIncomingResult.NO_CREDITOR -> throw notFound(
- "Customer $login not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- TalerAddIncomingResult.NOT_EXCHANGE -> throw conflict(
- "$login is not an exchange account.",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- TalerAddIncomingResult.NO_DEBITOR -> throw notFound(
- "Debitor account was not found",
- TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
- )
- TalerAddIncomingResult.SAME_ACCOUNT -> throw conflict(
- "Wire transfer attempted with credit and debit party being the same bank account",
- TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
- )
- TalerAddIncomingResult.BOTH_EXCHANGE -> throw conflict(
- "Wire transfer attempted with credit and debit party being both exchange account",
- TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
- )
- TalerAddIncomingResult.RESERVE_PUB_REUSE -> throw conflict(
- "reserve_pub used already",
- TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
- )
- TalerAddIncomingResult.BALANCE_INSUFFICIENT -> throw conflict(
- "Insufficient balance for debitor",
- TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
- )
- TalerAddIncomingResult.SUCCESS -> call.respond(
- AddIncomingResponse(
- timestamp = TalerProtocolTimestamp(timestamp),
- row_id = dbRes.txRowId!!
+ auth(db, TokenScope.readwrite) {
+ post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
+ val req = call.receive<AddIncomingRequest>()
+ ctx.checkInternalCurrency(req.amount)
+ val timestamp = Instant.now()
+ val dbRes = db.talerAddIncomingCreate(
+ req = req,
+ username = username,
+ timestamp = timestamp
+ )
+ when (dbRes.txResult) {
+ TalerAddIncomingResult.NO_CREDITOR -> throw notFound(
+ "Customer $username not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
)
- )
+ TalerAddIncomingResult.NOT_EXCHANGE -> throw conflict(
+ "$username is not an exchange account.",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ TalerAddIncomingResult.NO_DEBITOR -> throw notFound(
+ "Debitor account was not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ TalerAddIncomingResult.SAME_ACCOUNT -> throw conflict(
+ "Wire transfer attempted with credit and debit party being the same bank account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ TalerAddIncomingResult.BOTH_EXCHANGE -> throw conflict(
+ "Wire transfer attempted with credit and debit party being both exchange account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ TalerAddIncomingResult.RESERVE_PUB_REUSE -> throw conflict(
+ "reserve_pub used already",
+ TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
+ )
+ TalerAddIncomingResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient balance for debitor",
+ TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+ )
+ TalerAddIncomingResult.SUCCESS -> call.respond(
+ AddIncomingResponse(
+ timestamp = TalerProtocolTimestamp(timestamp),
+ row_id = dbRes.txRowId!!
+ )
+ )
+ }
}
}
}
\ 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
@@ -43,29 +43,10 @@ fun ApplicationCall.expectUriComponent(componentName: String) =
hint = "No username found in the URI", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
)
-typealias ResourceName = String
-
-/**
- * Factors out the retrieval of the resource name from
- * the URI. The resource looked for defaults to "USERNAME"
- * as this is frequently mentioned resource along the endpoints.
- *
- * This helper is recommended because it returns a ResourceName
- * type that then offers the ".canI()" helper to check if the user
- * has the rights on the resource.
- */
-fun ApplicationCall.getResourceName(param: String): ResourceName =
- this.expectUriComponent(param)
-
-
-/** Get account login from path */
-suspend fun ApplicationCall.accountLogin(): String = getResourceName("USERNAME")
-
/** Retrieve the bank account info for the selected username*/
suspend fun ApplicationCall.bankAccount(db: Database): BankAccount {
- val login = accountLogin()
- return db.bankAccountGetFromCustomerLogin(login) ?: throw notFound(
- hint = "Bank account for customer $login not found",
+ return db.bankAccountGetFromCustomerLogin(username) ?: throw notFound(
+ hint = "Bank account for customer $username not found",
talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
)
}
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -141,6 +141,7 @@ class CoreBankTokenApiTest {
// DELETE /accounts/USERNAME/token
@Test
fun delete() = bankSetup { _ ->
+ // TODO test restricted
val token = client.post("/accounts/merchant/token") {
basicAuth("merchant", "merchant-password")
jsonBody(json { "scope" to "readonly" })