[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] 02/02: Legacy API access control.
From: |
gnunet |
Subject: |
[libeufin] 02/02: Legacy API access control. |
Date: |
Mon, 05 Dec 2022 20:43:00 +0100 |
This is an automated email from the git hooks/post-receive script.
ms pushed a commit to branch master
in repository libeufin.
commit b556bd1b92b18f56eaf38d137d1792d800c13e88
Author: MS <ms@taler.net>
AuthorDate: Mon Dec 5 20:40:09 2022 +0100
Legacy API access control.
---
.../tech/libeufin/nexus/ebics/EbicsClient.kt | 1 -
.../main/kotlin/tech/libeufin/sandbox/Helpers.kt | 41 +++++-
.../src/main/kotlin/tech/libeufin/sandbox/JSON.kt | 10 ++
.../src/main/kotlin/tech/libeufin/sandbox/Main.kt | 145 +++++++++++++--------
4 files changed, 136 insertions(+), 61 deletions(-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
index 95f6f5ec..bc2233b1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -35,7 +35,6 @@ import java.util.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util")
private suspend inline fun HttpClient.postToBank(url: String, body: String):
String {
- // logger.debug("Posting: $body")
if (!XMLUtil.validateFromString(body)) throw NexusError(
HttpStatusCode.InternalServerError,
"EBICS (outgoing) document is invalid"
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
index e96f14b9..a0617abc 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -19,9 +19,6 @@
package tech.libeufin.sandbox
-import com.fasterxml.jackson.core.JsonParseException
-import com.fasterxml.jackson.databind.exc.MismatchedInputException
-import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import io.ktor.application.*
import io.ktor.http.HttpStatusCode
import io.ktor.request.*
@@ -29,6 +26,7 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
import tech.libeufin.util.*
+import java.awt.Label
import java.math.BigDecimal
import java.security.interfaces.RSAPublicKey
import java.util.*
@@ -48,6 +46,23 @@ data class SandboxCamt(
val creationTime: Long
)
+/**
+ *
+ * Return true if access to the bank account can be granted,
+ * false otherwise.
+ *
+ * Given the policy of having bank account names matching
+ * their owner's username, this function enforces such policy
+ * with the exception that 'admin' can access every bank
+ * account. A null username indicates disabled authentication
+ * checks, hence it grants the access.
+ */
+fun allowOwnerOrAdmin(username: String?, bankAccountLabel: String): Boolean {
+ if (username == null) return true
+ if (username == "admin") return true
+ return username == bankAccountLabel
+}
+
/**
* Throws exception if the credentials are wrong.
*
@@ -55,14 +70,14 @@ data class SandboxCamt(
* - null if the authentication is disabled (during tests, for example).
* This facilitates tests because allows requests to lack entirely a
* Authorization header.
- * - the name of the authenticated user
+ * - the username of the authenticated user
* - throw exception when the authentication fails
*
* Note: at this point it is ONLY checked whether the user provided
* a valid password for the username mentioned in the Authorization header.
* The actual access to the resources must be later checked by each handler.
*/
-fun ApplicationRequest.basicAuth(): String? {
+fun ApplicationRequest.basicAuth(onlyAdmin: Boolean = false): String? {
val withAuth = this.call.ensureAttribute(WITH_AUTH_ATTRIBUTE_KEY)
if (!withAuth) {
logger.info("Authentication is disabled - assuming tests currently
running.")
@@ -77,6 +92,10 @@ fun ApplicationRequest.basicAuth(): String? {
)
return credentials.first
}
+ /**
+ * If only admin auth was allowed, here it failed already,
+ * hence throw 401. */
+ if (onlyAdmin) throw unauthorized("Only admin allowed.")
val passwordHash = transaction {
val customer = getCustomer(credentials.first)
customer.passwordHash
@@ -131,7 +150,15 @@ fun getHistoryElementFromTransactionRow(
return getHistoryElementFromTransactionRow(dbRow.transactionRef)
}
-// Need to be called within a transaction {} block.
+/**
+ * Need to be called within a transaction {} block. It
+ * is acceptable to pass a bank account's label as the
+ * parameter, because usernames can only own one bank
+ * account whose label equals the owner's username.
+ *
+ * Future versions may relax this policy to allow one
+ * customer to own multiple bank accounts.
+ */
fun getCustomer(username: String): DemobankCustomerEntity {
return DemobankCustomerEntity.find {
DemobankCustomersTable.username eq username
@@ -378,7 +405,7 @@ fun getEbicsSubscriberFromDetails(userID: String,
partnerID: String, hostID: Str
(EbicsSubscribersTable.hostId eq hostID)
}.firstOrNull() ?: throw SandboxError(
HttpStatusCode.NotFound,
- "Ebics subscriber not found"
+ "Ebics subscriber (${userID}, ${partnerID}, ${hostID}) not found"
)
}
}
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
index 0772919a..3ed0eba4 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
@@ -82,12 +82,22 @@ data class EbicsSubscriberObsoleteApi(
val userID: String,
val systemID: String? = null
)
+
+/**
+ * Allows the admin to associate a new bank account
+ * to a EBICS subscriber.
+ */
data class EbicsBankAccountRequest(
val subscriber: EbicsSubscriberObsoleteApi,
val iban: String,
val bic: String,
val name: String,
val label: String,
+ /**
+ * Customer username that will own this
+ * EBICS subscriber.
+ */
+ val owner: String
)
data class CustomerRegistration(
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index b5592350..91e7c312 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -83,7 +83,6 @@ import javax.xml.bind.JAXBContext
import kotlin.system.exitProcess
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox")
-private val currencyEnv: String? = System.getenv("LIBEUFIN_SANDBOX_CURRENCY")
const val SANDBOX_DB_ENV_VAR_NAME = "LIBEUFIN_SANDBOX_DB_CONNECTION"
private val adminPassword: String? =
System.getenv("LIBEUFIN_SANDBOX_ADMIN_PASSWORD")
var WITH_AUTH = true // Needed by helpers too, hence not making it private.
@@ -588,16 +587,18 @@ val sandboxApp: Application.() -> Unit = {
ContentType.Text.Plain
)
}
-
// Respond with the last statement of the requesting account.
// Query details in the body.
post("/admin/payments/camt") {
- call.request.basicAuth()
+ val username = call.request.basicAuth()
val body = call.receiveJson<CamtParams>()
if (body.type != 53) throw SandboxError(
HttpStatusCode.NotFound,
"Only Camt.053 documents can be generated."
)
+ if (!allowOwnerOrAdmin(username, body.bankaccount))
+ throw unauthorized("User '${username}' has no rights over" +
+ " bank account '${body.bankaccount}'")
val camtMessage = transaction {
val bankaccount = getBankAccountFromLabel(
body.bankaccount,
@@ -616,16 +617,34 @@ val sandboxApp: Application.() -> Unit = {
return@post
}
- // create a new bank account, no EBICS relation.
+ /**
+ * Create a new bank account, no EBICS relation. Okay
+ * to let a user, since having a particular username allocates
+ * already a bank account with such label.
+ */
post("/admin/bank-accounts/{label}") {
val username = call.request.basicAuth()
val body = call.receiveJson<BankAccountInfo>()
+ if (!allowOwnerOrAdmin(username, body.label))
+ throw unauthorized("User '$username' has no rights over" +
+ " bank account '${body.label}'"
+ )
transaction {
+ val maybeBankAccount = BankAccountEntity.find {
+ BankAccountsTable.label eq body.label
+ }.firstOrNull()
+ if (maybeBankAccount != null)
+ throw conflict("Bank account '${body.label}' exist
already")
BankAccountEntity.new {
iban = body.iban
bic = body.bic
label = body.label
- owner = username ?: "admin" // allows
+ /**
+ * The null username case exist when auth is
+ * disabled. In this case, we assign the bank
+ * account to 'admin'.
+ */
+ owner = username ?: "admin"
demoBank = getDefaultDemobank()
}
}
@@ -635,11 +654,13 @@ val sandboxApp: Application.() -> Unit = {
// Information about one bank account.
get("/admin/bank-accounts/{label}") {
- call.request.basicAuth()
+ val username = call.request.basicAuth()
val label = call.getUriComponent("label")
val ret = transaction {
val demobank = getDefaultDemobank()
val bankAccount = getBankAccountFromLabel(label, demobank)
+ if (!allowOwnerOrAdmin(username, label))
+ throw unauthorized("'${username}' has no rights over
'$label'")
val balance = balanceForAccount(bankAccount)
object {
val balance = "${bankAccount.demoBank.currency}:${balance}"
@@ -655,9 +676,8 @@ val sandboxApp: Application.() -> Unit = {
// Book one incoming payment for the requesting account.
// The debtor is not required to have an account at this Sandbox.
post("/admin/bank-accounts/{label}/simulate-incoming-transaction") {
- val username = call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val body = call.receiveJson<IncomingPaymentInfo>()
- // FIXME: generate nicer UUID!
val accountLabel = ensureNonNull(call.parameters["label"])
val reqDebtorBic = body.debtorBic
if (reqDebtorBic != null && !validateBic(reqDebtorBic)) {
@@ -680,10 +700,11 @@ val sandboxApp: Application.() -> Unit = {
accountLabel, demobank
)
val randId = getRandomString(16)
+ val customer = getCustomer(accountLabel)
BankAccountTransactionEntity.new {
creditorIban = account.iban
creditorBic = account.bic
- creditorName = getPersonNameFromCustomer(username)
+ creditorName = customer.name ?: "Name not given."
debtorIban = body.debtorIban
debtorBic = reqDebtorBic
debtorName = body.debtorName
@@ -701,8 +722,13 @@ val sandboxApp: Application.() -> Unit = {
}
// Associates a new bank account with an existing Ebics subscriber.
post("/admin/ebics/bank-accounts") {
- val username = call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val body = call.receiveJson<EbicsBankAccountRequest>()
+ if (body.owner != body.label)
+ throw conflict(
+ "Customer username '${body.owner}'" +
+ " differs from bank account name '${body.label}'"
+ )
if (!validateBic(body.bic)) {
throw SandboxError(HttpStatusCode.BadRequest, "invalid BIC
(${body.bic})")
}
@@ -712,8 +738,9 @@ val sandboxApp: Application.() -> Unit = {
body.subscriber.partnerID,
body.subscriber.hostID
)
+ if (subscriber.bankAccount != null)
+ throw conflict("subscriber has already a bank account:
${subscriber.bankAccount?.label}")
val demobank = getDefaultDemobank()
-
/**
* Checking that the default demobank doesn't have already the
* requested IBAN and bank account label.
@@ -733,7 +760,7 @@ val sandboxApp: Application.() -> Unit = {
iban = body.iban
bic = body.bic
label = body.label
- owner = username ?: "admin"
+ owner = body.owner
demoBank = demobank
}
}
@@ -743,7 +770,7 @@ val sandboxApp: Application.() -> Unit = {
// Information about all the default demobank's bank accounts
get("/admin/bank-accounts") {
- call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val accounts = mutableListOf<BankAccountInfo>()
transaction {
val demobank = getDefaultDemobank()
@@ -765,49 +792,52 @@ val sandboxApp: Application.() -> Unit = {
// Details of all the transactions of one bank account.
get("/admin/bank-accounts/{label}/transactions") {
- call.request.basicAuth()
+ val username = call.request.basicAuth()
val ret = AccountTransactions()
+ val accountLabel = ensureNonNull(call.parameters["label"])
+ if (!allowOwnerOrAdmin(username, accountLabel))
+ throw unauthorized("Requesting user '${username}'" +
+ " has no rights over bank account '${accountLabel}'"
+ )
transaction {
- val accountLabel = ensureNonNull(call.parameters["label"])
- transaction {
- val demobank = getDefaultDemobank()
- val account = getBankAccountFromLabel(accountLabel,
demobank)
- BankAccountTransactionEntity.find {
- BankAccountTransactionsTable.account eq account.id
- }.forEach {
- ret.payments.add(
- PaymentInfo(
- accountLabel = account.label,
- creditorIban = it.creditorIban,
- accountServicerReference =
it.accountServicerReference,
- paymentInformationId = it.pmtInfId,
- debtorIban = it.debtorIban,
- subject = it.subject,
- date = GMTDate(it.date).toHttpDate(),
- amount = it.amount,
- creditorBic = it.creditorBic,
- creditorName = it.creditorName,
- debtorBic = it.debtorBic,
- debtorName = it.debtorName,
- currency = it.currency,
- creditDebitIndicator = when (it.direction) {
- "CRDT" -> "credit"
- "DBIT" -> "debit"
- else -> throw Error("invalid direction")
- }
- )
+ val demobank = getDefaultDemobank()
+ val account = getBankAccountFromLabel(accountLabel, demobank)
+ BankAccountTransactionEntity.find {
+ BankAccountTransactionsTable.account eq account.id
+ }.forEach {
+ ret.payments.add(
+ PaymentInfo(
+ accountLabel = account.label,
+ creditorIban = it.creditorIban,
+ accountServicerReference =
it.accountServicerReference,
+ paymentInformationId = it.pmtInfId,
+ debtorIban = it.debtorIban,
+ subject = it.subject,
+ date = GMTDate(it.date).toHttpDate(),
+ amount = it.amount,
+ creditorBic = it.creditorBic,
+ creditorName = it.creditorName,
+ debtorBic = it.debtorBic,
+ debtorName = it.debtorName,
+ currency = it.currency,
+ creditDebitIndicator = when (it.direction) {
+ "CRDT" -> "credit"
+ "DBIT" -> "debit"
+ else -> throw Error("invalid direction")
+ }
)
- }
+ )
}
}
call.respond(ret)
}
-
- // Generate one incoming and one outgoing transactions for
- // one bank account. Counterparts do not need to have an account
- // at this Sandbox.
+ /**
+ * Generate one incoming and one outgoing transactions for
+ * one bank account. Counterparts do not need to have an account
+ * at this Sandbox.
+ */
post("/admin/bank-accounts/{label}/generate-transactions") {
- call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
transaction {
val accountLabel = ensureNonNull(call.parameters["label"])
val demobank = getDefaultDemobank()
@@ -865,9 +895,18 @@ val sandboxApp: Application.() -> Unit = {
* user is allowed to call this.
*/
post("/admin/ebics/subscribers") {
- call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val body = call.receiveJson<EbicsSubscriberObsoleteApi>()
transaction {
+ // Check it exists first.
+ val maybeSubscriber = EbicsSubscriberEntity.find {
+ EbicsSubscribersTable.userId eq body.userID and (
+ EbicsSubscribersTable.partnerId eq body.partnerID
+ ) and (
+ EbicsSubscribersTable.systemId eq body.systemID
+ )
+ }.firstOrNull()
+ if (maybeSubscriber != null) throw conflict("EBICS subscriber
exists already")
EbicsSubscriberEntity.new {
partnerId = body.partnerID
userId = body.userID
@@ -886,7 +925,7 @@ val sandboxApp: Application.() -> Unit = {
// Shows details of all the EBICS subscribers of this Sandbox.
get("/admin/ebics/subscribers") {
- call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val ret = AdminGetSubscribers()
transaction {
EbicsSubscriberEntity.all().forEach {
@@ -906,7 +945,7 @@ val sandboxApp: Application.() -> Unit = {
// Change keys used in the EBICS communications.
post("/admin/ebics/hosts/{hostID}/rotate-keys") {
- call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val hostID: String = call.parameters["hostID"] ?: throw
SandboxError(
io.ktor.http.HttpStatusCode.BadRequest, "host ID missing in
URL"
)
@@ -933,7 +972,7 @@ val sandboxApp: Application.() -> Unit = {
// Create a new EBICS host
post("/admin/ebics/hosts") {
- call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val req = call.receiveJson<EbicsHostCreateRequest>()
val pairA = CryptoUtil.generateRsaKeyPair(2048)
val pairB = CryptoUtil.generateRsaKeyPair(2048)
@@ -957,7 +996,7 @@ val sandboxApp: Application.() -> Unit = {
// Show the names of all the Ebics hosts
get("/admin/ebics/hosts") {
- call.request.basicAuth()
+ call.request.basicAuth(onlyAdmin = true)
val ebicsHosts = transaction {
EbicsHostEntity.all().map { it.hostId }
}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.