commit 01e4e216098a7c650c45b28c6adf5a0f27f3a1ac
parent f9bac0535f6891afa21842a001075927d0402f0f
Author: ms <ms@taler.net>
Date: Tue, 19 Oct 2021 09:20:04 +0200
implement 'demobank' resource
Diffstat:
7 files changed, 130 insertions(+), 152 deletions(-)
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Auth.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Auth.kt
@@ -1,47 +0,0 @@
-package tech.libeufin.sandbox
-
-import UtilError
-import io.ktor.application.*
-import io.ktor.http.*
-import io.ktor.request.*
-import jdk.jshell.execution.Util
-import org.jetbrains.exposed.sql.transactions.transaction
-import org.slf4j.LoggerFactory
-import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.LibeufinErrorCode
-import tech.libeufin.util.getHTTPBasicAuthCredentials
-
-private val logger = LoggerFactory.getLogger("tech.libeufin.util")
-/**
- * HTTP basic auth. Throws error if password is wrong,
- * and makes sure that the user exists in the system.
- *
- * @return user entity
- */
-fun authenticateRequest(request: ApplicationRequest): SandboxUserEntity {
- return transaction {
- val (username, password) = getHTTPBasicAuthCredentials(request)
- val user = SandboxUserEntity.find {
- SandboxUsersTable.username eq username
- }.firstOrNull()
- if (user == null) {
- throw UtilError(
- HttpStatusCode.Unauthorized,
- "Unknown user '$username'",
- LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
- )
- }
- CryptoUtil.checkPwOrThrow(password, user.passwordHash)
- user
- }
-}
-
-fun requireSuperuser(request: ApplicationRequest): SandboxUserEntity {
- return transaction {
- val user = authenticateRequest(request)
- if (!user.superuser) {
- throw SandboxError(HttpStatusCode.Forbidden, "must be superuser")
- }
- user
- }
-}
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
@@ -88,64 +88,33 @@ enum class KeyState {
}
// FIXME: This should be DemobankConfigTable!
-object SandboxConfigsTable : LongIdTable() {
+object DemobankConfigsTable : LongIdTable() {
val currency = text("currency")
val allowRegistrations = bool("allowRegistrations")
val bankDebtLimit = integer("bankDebtLimit")
val usersDebtLimit = integer("usersDebtLimit")
- val hostname = text("hostname")
+ val name = text("hostname")
}
-class SandboxConfigEntity(id: EntityID<Long>) : LongEntity(id) {
- companion object : LongEntityClass<SandboxConfigEntity>(SandboxConfigsTable)
- var currency by SandboxConfigsTable.currency
- var allowRegistrations by SandboxConfigsTable.allowRegistrations
- var bankDebtLimit by SandboxConfigsTable.bankDebtLimit
- var usersDebtLimit by SandboxConfigsTable.usersDebtLimit
- var hostname by SandboxConfigsTable.hostname
+class DemobankConfigEntity(id: EntityID<Long>) : LongEntity(id) {
+ companion object : LongEntityClass<DemobankConfigEntity>(DemobankConfigsTable)
+ var currency by DemobankConfigsTable.currency
+ var allowRegistrations by DemobankConfigsTable.allowRegistrations
+ var bankDebtLimit by DemobankConfigsTable.bankDebtLimit
+ var usersDebtLimit by DemobankConfigsTable.usersDebtLimit
+ var name by DemobankConfigsTable.name
}
/**
- * Currently, this entity is never associated with a bank account,
- * as those get only paired with Ebics subscribers! Eventually, a
- * Ebics subscriber should map to a SandboxUserEntity that in turn
- * will own bank accounts.
- *
- * FIXME: Do we really need normal users and superusers for the sandbox?
- * => Nope, we don't even want user management for the sandbox!
- * => This table must be killed, instead we just read the admin token via env variable
- * and use a fixed "admin" user name.
- */
-object SandboxUsersTable : LongIdTable() {
- val username = text("username")
- val passwordHash = text("password")
- val superuser = bool("superuser") // admin
- /**
- * Some users may only have an administrative role in the system,
- * therefore do not need a bank account.
- */
- val bankAccount = reference("bankAccount", BankAccountsTable).nullable()
-}
-
-class SandboxUserEntity(id: EntityID<Long>) : LongEntity(id) {
- companion object : LongEntityClass<SandboxUserEntity>(SandboxUsersTable)
- var username by SandboxUsersTable.username
- var passwordHash by SandboxUsersTable.passwordHash
- var superuser by SandboxUsersTable.superuser
- var bankAccount by BankAccountEntity optionalReferencedOn SandboxUsersTable.bankAccount
-}
-
-
-/**
* Users who are allowed to log into the demo bank.
* Created via the /demobanks/{demobankname}/register endpoint.
*/
-object DemobankUsersTable : LongIdTable() {
- // FIXME: ...
- // var isPublic = ...
- // var demobankConfig (=> which demobank is this user part of)
- // ...
- // FIXME: Must have a mandatory foreign key reference into BankAccountTransactionsTable
+object DemobankCustomersTable : LongIdTable() {
+ val isPublic = bool("isPublic").default(false)
+ val demobankConfig = reference("demobankConfig", DemobankConfigsTable)
+ val balance = text("balance")
+ val username = text("username")
+ val passwordHash = text("passwordHash")
}
@@ -479,8 +448,7 @@ fun dbDropTables(dbConnectionString: String) {
BankAccountsTable,
BankAccountReportsTable,
BankAccountStatementsTable,
- SandboxConfigsTable,
- SandboxUsersTable,
+ DemobankConfigsTable,
TalerWithdrawalsTable
)
}
@@ -491,8 +459,7 @@ fun dbCreateTables(dbConnectionString: String) {
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
transaction {
SchemaUtils.create(
- SandboxConfigsTable,
- SandboxUsersTable,
+ DemobankConfigsTable,
EbicsSubscribersTable,
EbicsHostsTable,
EbicsDownloadTransactionsTable,
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -87,24 +87,16 @@ fun getBankAccountFromSubscriber(subscriber: EbicsSubscriberEntity): BankAccount
}
}
-/**
- * Fetch a configuration for Sandbox, corresponding to the host that runs the service.
- */
-fun getSandboxConfig(hostname: String?): SandboxConfigEntity {
- var ret: SandboxConfigEntity? = transaction {
- if (hostname == null) {
- SandboxConfigEntity.all().firstOrNull()
+fun getSandboxConfig(name: String?): DemobankConfigEntity? {
+ return transaction {
+ if (name == null) {
+ DemobankConfigEntity.all().firstOrNull()
} else {
- SandboxConfigEntity.find {
- SandboxConfigsTable.hostname eq hostname
+ DemobankConfigEntity.find {
+ DemobankConfigsTable.name eq name
}.firstOrNull()
}
}
- if (ret == null) throw SandboxError(
- HttpStatusCode.InternalServerError,
- "Serving from a non configured host"
- )
- return ret
}
fun getEbicsSubscriberFromDetails(userID: String, partnerID: String, hostID: String): EbicsSubscriberEntity {
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt
@@ -22,6 +22,13 @@ package tech.libeufin.sandbox
import tech.libeufin.util.PaymentInfo
import tech.libeufin.util.RawPayment
+data class Demobank(
+ val currency: String,
+ val name: String,
+ val userDebtLimit: Int,
+ val bankDebtLimit: Int,
+ val allowRegistrations: Boolean
+)
/**
* Used to show the list of Ebics hosts that exist
* in the system.
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -104,8 +104,8 @@ class Config : CliktCommand("Insert one configuration into the database") {
}
}
- private val hostnameOption by argument(
- "HOSTNAME", help = "hostname that serves this configuration"
+ private val nameOption by argument(
+ "NAME", help = "Name of this configuration"
)
private val currencyOption by option("--currency").default("EUR")
private val bankDebtLimitOption by option("--bank-debt-limit").int().default(1000000)
@@ -120,12 +120,19 @@ class Config : CliktCommand("Insert one configuration into the database") {
execThrowableOrTerminate {
dbCreateTables(dbConnString)
transaction {
- SandboxConfigEntity.new {
+ val checkExist = DemobankConfigEntity.find {
+ DemobankConfigsTable.name eq nameOption
+ }.firstOrNull()
+ if (checkExist != null) {
+ println("Error, demobank ${nameOption} exists already, not overriding it.")
+ exitProcess(1)
+ }
+ DemobankConfigEntity.new {
currency = currencyOption
bankDebtLimit = bankDebtLimitOption
usersDebtLimit = usersDebtLimitOption
allowRegistrations = allowRegistrationsOption
- hostname = hostnameOption
+ name = nameOption
}
}
}
@@ -254,6 +261,11 @@ class Serve : CliktCommand("Run sandbox HTTP server") {
override fun run() {
WITH_AUTH = auth
setLogLevel(logLevel)
+ if (WITH_AUTH && adminPassword == null) {
+ println("Error: auth is enabled, but env LIBEUFIN_SANDBOX_ADMIN_PASSWORD is not."
+ + " (Option --no-auth exists for tests)")
+ exitProcess(1)
+ }
execThrowableOrTerminate { dbCreateTables(getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)) }
if (withUnixSocket != null) {
startServer(
@@ -266,6 +278,15 @@ class Serve : CliktCommand("Run sandbox HTTP server") {
}
}
+private fun getJsonFromDemobankConfig(fromDb: DemobankConfigEntity): Demobank {
+ return Demobank(
+ currency = fromDb.currency,
+ userDebtLimit = fromDb.usersDebtLimit,
+ bankDebtLimit = fromDb.bankDebtLimit,
+ allowRegistrations = fromDb.allowRegistrations,
+ name = fromDb.name
+ )
+}
fun findEbicsSubscriber(partnerID: String, userID: String, systemID: String?): EbicsSubscriberEntity? {
return if (systemID == null) {
EbicsSubscriberEntity.find {
@@ -460,7 +481,9 @@ val sandboxApp: Application.() -> Unit = {
if(adminPassword == null) {
throw internalServerError(
"Sandbox has no admin password defined." +
- " Please define LIBEUFIN_SANDBOX_ADMIN_PASSWORD in the environment."
+ " Please define LIBEUFIN_SANDBOX_ADMIN_PASSWORD in the environment, " +
+ "or launch with --no-auth."
+
)
}
ac.attributes.put(
@@ -1018,33 +1041,37 @@ val sandboxApp: Application.() -> Unit = {
// debt limit and possibly other configuration
// (could also be a CLI command for now)
post("/demobanks") {
-
+ throw NotImplementedError("Only available in the CLI.")
}
- // List configured demobanks
get("/demobanks") {
-
- }
-
- delete("/demobank/{demobankid") {
-
+ expectAdmin(call.request.basicAuth())
+ val ret = object { val demoBanks = mutableListOf<Demobank>() }
+ transaction {
+ DemobankConfigEntity.all().forEach {
+ ret.demoBanks.add(getJsonFromDemobankConfig(it))
+ }
+ }
+ call.respond(ret)
+ return@get
}
- get("/demobank/{demobankid") {
-
+ get("/demobanks/{demobankid}") {
+ expectAdmin(call.request.basicAuth())
+ val demobankId = call.getUriComponent("demobankid")
+ val ret: DemobankConfigEntity = transaction {
+ DemobankConfigEntity.find {
+ DemobankConfigsTable.name eq demobankId
+ }.firstOrNull()
+ } ?: throw notFound("Demobank ${demobankId} not found")
+ call.respond(getJsonFromDemobankConfig(ret))
+ return@get
}
- route("/demobank/{demobankid}") {
- // Note: Unlike the old pybank, the sandbox does *not* actually expose the
- // taler wire gateway API, because the exchange uses the nexus.
-
- // Endpoint(s) for making arbitrary payments in the sandbox for integration tests
- // FIXME: Do we actually need this, or can we just use the sandbox admin APIs?
- route("/testing-api") {
-
- }
+ route("/demobanks/{demobankid}") {
route("/access-api") {
+
get("/accounts/{account_name}") {
// Authenticated. Accesses basic information (balance)
// about an account. (see docs)
@@ -1068,10 +1095,8 @@ val sandboxApp: Application.() -> Unit = {
// Get transaction history of a public account
}
- post("/testing/register") {
- // Register a new account.
- // No authentication is required to register a new user.
- // FIXME: Should probably not use "testing" as the prefix, since it's used "in production" in the demobank SPA
+ post("/register") {
+
}
}
diff --git a/util/src/main/kotlin/Config.kt b/util/src/main/kotlin/Config.kt
@@ -92,10 +92,10 @@ fun getValueFromEnv(varName: String): String? {
fun getDbConnFromEnv(varName: String): String {
val dbConnStr = System.getenv(varName)
if (dbConnStr.isNullOrBlank() or dbConnStr.isNullOrEmpty()) {
- printLnErr("DB connection string not found/valid in the env variable $varName.")
- printLnErr("The following two examples are valid connection strings:")
- printLnErr("jdbc:sqlite:/tmp/libeufindb.sqlite3")
- printLnErr("jdbc:postgresql://localhost:5432/libeufindb?user=Foo&password=secret")
+ printLnErr("\nError: DB connection string undefined or invalid in the env variable $varName.")
+ printLnErr("\nThe following two examples are valid connection strings:")
+ printLnErr("\njdbc:sqlite:/tmp/libeufindb.sqlite3")
+ printLnErr("jdbc:postgresql://localhost:5432/libeufindb?user=Foo&password=secret\n")
exitProcess(1)
}
return dbConnStr
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
@@ -16,7 +16,11 @@ private fun unauthorized(msg: String): UtilError {
)
}
-
+fun notFound(msg: String): UtilError {
+ return UtilError(
+ HttpStatusCode.NotFound, msg, LibeufinErrorCode.LIBEUFIN_EC_NONE
+ )
+}
/**
* Returns the token (including the 'secret-token:' prefix)
@@ -48,6 +52,14 @@ fun internalServerError(
)
}
+fun badRequest(msg: String): UtilError {
+ return UtilError(
+ HttpStatusCode.BadRequest,
+ msg,
+ ec = LibeufinErrorCode.LIBEUFIN_EC_NONE
+ )
+}
+
/**
* Get the base URL of a request; handles proxied case.
*/
@@ -81,18 +93,25 @@ fun ApplicationRequest.getBaseUrl(): String {
}
/**
- * Authenticate the HTTP request with a given token. This one
- * is expected to comply with the RFC 8959 format; the function
- * throws an exception when the authentication fails
- *
- * @param tokenEnv is the authorization token that was found in the
- * environment.
+ * Get the URI (path's) component or throw Internal server error.
+ * @param component the name of the URI component to return.
+ */
+fun ApplicationCall.getUriComponent(name: String): String {
+ val ret: String? = this.parameters[name]
+ if (ret == null) throw internalServerError("Component $name not found in URI")
+ return ret
+}
+/**
+ * Return:
+ * - null if the authentication is disabled (during tests, for example)
+ * - the name of the authenticated user
+ * - throw exception when the authentication fails
*/
-fun ApplicationRequest.basicAuth() {
+fun ApplicationRequest.basicAuth(): String? {
val withAuth = this.call.ensureAttribute(WITH_AUTH_ATTRIBUTE_KEY)
if (!withAuth) {
logger.info("Authentication is disabled - assuming tests currently running.")
- return
+ return null
}
val credentials = getHTTPBasicAuthCredentials(this)
if (credentials.first == "admin") {
@@ -101,7 +120,7 @@ fun ApplicationRequest.basicAuth() {
if (credentials.second != adminPassword) throw unauthorized(
"Admin authentication failed"
)
- return
+ return credentials.first
}
throw unauthorized("Demobank customers not implemented yet!")
/**
@@ -109,6 +128,20 @@ fun ApplicationRequest.basicAuth() {
*/
}
+/**
+ * Throw "unauthorized" if the request is not
+ * authenticated by "admin", silently return otherwise.
+ *
+ * @param username who made the request.
+ */
+fun expectAdmin(username: String?) {
+ if (username == null) {
+ logger.info("Skipping 'admin' authentication for tests.")
+ return
+ }
+ if (username != "admin") throw unauthorized("Only admin allowed: $username is not.")
+}
+
fun getHTTPBasicAuthCredentials(request: ApplicationRequest): Pair<String, String> {
val authHeader = getAuthorizationHeader(request)
return extractUserAndPassword(authHeader)
@@ -119,6 +152,7 @@ fun getHTTPBasicAuthCredentials(request: ApplicationRequest): Pair<String, Strin
*/
fun getAuthorizationHeader(request: ApplicationRequest): String {
val authorization = request.headers["Authorization"]
+ logger.debug("Found Authorization header: $authorization")
return authorization ?: throw UtilError(
HttpStatusCode.BadRequest, "Authorization header not found",
LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED