diff options
author | Antoine A <> | 2023-10-13 10:43:11 +0000 |
---|---|---|
committer | Antoine A <> | 2023-10-13 10:45:59 +0000 |
commit | d7064c851ae27cd5495f509fbc191df710e9e724 (patch) | |
tree | d0e7a05360286ec82ba8a4c6783f77887170ec3b | |
parent | 11d4d41bf20465259d245c64d50c676308216976 (diff) | |
download | libeufin-d7064c851ae27cd5495f509fbc191df710e9e724.tar.gz libeufin-d7064c851ae27cd5495f509fbc191df710e9e724.tar.bz2 libeufin-d7064c851ae27cd5495f509fbc191df710e9e724.zip |
Improve and fix config logic
move all config logic in a single file
log and exit on config error
add currencies.conf
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | bank/conf/test.conf | 11 | ||||
-rw-r--r-- | bank/conf/test_restrict.conf | 11 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Config.kt | 117 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Database.kt | 26 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 105 | ||||
-rw-r--r-- | bank/src/test/kotlin/helpers.kt | 14 | ||||
-rw-r--r-- | contrib/currencies.conf | 99 |
8 files changed, 252 insertions, 132 deletions
@@ -46,6 +46,7 @@ deb: exec-arch copy-spa install: install -d $(config_dir) install contrib/libeufin-bank.conf $(config_dir)/ + install contrib/currencies.conf $(config_dir)/ install -D database-versioning/libeufin-bank*.sql -t $(sql_dir) install -D database-versioning/versioning.sql -t $(sql_dir) install -D database-versioning/procedures.sql -t $(sql_dir) diff --git a/bank/conf/test.conf b/bank/conf/test.conf index 703ab6c5..80bfe99a 100644 --- a/bank/conf/test.conf +++ b/bank/conf/test.conf @@ -5,17 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 REGISTRATION_BONUS_ENABLED = NO SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com -[currency-kudos] -ENABLED = YES -name = "Kudos (Taler Demonstrator)" -code = "KUDOS" -decimal_separator = "," -fractional_input_digits = 2 -fractional_normal_digits = 2 -fractional_trailing_zero_digits = 2 -is_currency_name_leading = NO -alt_unit_names = {"0":"ク"} - [libeufin-bankdb-postgres] SQL_DIR = $DATADIR/sql/ CONFIG = postgresql:///libeufincheck
\ No newline at end of file diff --git a/bank/conf/test_restrict.conf b/bank/conf/test_restrict.conf index 9a74f807..eda2037c 100644 --- a/bank/conf/test_restrict.conf +++ b/bank/conf/test_restrict.conf @@ -5,17 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 REGISTRATION_BONUS_ENABLED = NO restrict_registration = YES -[currency-kudos] -ENABLED = YES -name = "Kudos (Taler Demonstrator)" -code = "KUDOS" -decimal_separator = "," -fractional_input_digits = 2 -fractional_normal_digits = 2 -fractional_trailing_zero_digits = 2 -is_currency_name_leading = NO -alt_unit_names = {"0":"ク"} - [libeufin-bankdb-postgres] SQL_DIR = $DATADIR/sql/ CONFIG = postgresql:///libeufincheck
\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt new file mode 100644 index 00000000..ba9edb7f --- /dev/null +++ b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -0,0 +1,117 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ +package tech.libeufin.bank + +import ConfigSource +import TalerConfig +import TalerConfigError +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlinx.serialization.json.Json + +private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Config") +private val BANK_CONFIG_SOURCE = ConfigSource("libeufin-bank", "libeufin-bank") + +data class DatabaseConfig( + val dbConnStr: String, + val sqlDir: String +) + +data class ServerConfig( + val method: String, + val port: Int +) + +fun talerConfig(configPath: String?): TalerConfig = catchError { + val config = TalerConfig(BANK_CONFIG_SOURCE) + config.load(configPath) + config +} + +fun TalerConfig.loadDbConfig(): DatabaseConfig = catchError { + DatabaseConfig( + dbConnStr = requireString("libeufin-bankdb-postgres", "config"), + sqlDir = requirePath("libeufin-bankdb-postgres", "sql_dir") + ) +} + +fun TalerConfig.loadServerConfig(): ServerConfig = catchError { + ServerConfig( + method = requireString("libeufin-bank", "serve"), + port = requireNumber("libeufin-bank", "port") + ) +} + +fun TalerConfig.loadBankApplicationContext(): BankApplicationContext = catchError { + val currency = requireString("libeufin-bank", "currency") + val currencySpecification = sections.find { + it.startsWith("CURRENCY-") && requireBoolean(it, "enabled") && requireString(it, "code") == currency + }?.let { loadCurrencySpecification(it) } ?: throw TalerConfigError("missing currency specification for $currency") + BankApplicationContext( + currency = currency, + restrictRegistration = lookupBoolean("libeufin-bank", "restrict_registration") ?: false, + cashoutCurrency = lookupString("libeufin-bank", "cashout_currency"), + defaultCustomerDebtLimit = requireAmount("libeufin-bank", "default_customer_debt_limit", currency), + registrationBonusEnabled = lookupBoolean("libeufin-bank", "registration_bonus_enabled") ?: false, + registrationBonus = requireAmount("libeufin-bank", "registration_bonus", currency), + suggestedWithdrawalExchange = lookupString("libeufin-bank", "suggested_withdrawal_exchange"), + defaultAdminDebtLimit = requireAmount("libeufin-bank", "default_admin_debt_limit", currency), + spaCaptchaURL = lookupString("libeufin-bank", "spa_captcha_url"), + restrictAccountDeletion = lookupBoolean("libeufin-bank", "restrict_account_deletion") ?: true, + currencySpecification = currencySpecification + ) +} + +private fun TalerConfig.loadCurrencySpecification(section: String): CurrencySpecification = catchError { + CurrencySpecification( + name = requireString(section, "name"), + decimal_separator = requireString(section, "decimal_separator"), + num_fractional_input_digits = requireNumber(section, "fractional_input_digits"), + num_fractional_normal_digits = requireNumber(section, "fractional_normal_digits"), + num_fractional_trailing_zero_digits = requireNumber(section, "fractional_trailing_zero_digits"), + is_currency_name_leading = requireBoolean(section, "is_currency_name_leading"), + alt_unit_names = Json.decodeFromString(requireString(section, "alt_unit_names")) + ) +} + +private fun TalerConfig.requireAmount(section: String, option: String, currency: String): TalerAmount = catchError { + val amountStr = lookupString(section, option) ?: + throw TalerConfigError("expected amount for section $section, option $option, but config value is empty") + val amount = try { + TalerAmount(amountStr) + } catch (e: Exception) { + throw TalerConfigError("expected amount for section $section, option $option, but amount is malformed") + } + + if (amount.currency != currency) { + throw TalerConfigError( + "expected amount for section $section, option $option, but currency is wrong (got ${amount.currency} expected $currency" + ) + } + amount +} + +private fun <R> catchError(lambda: () -> R): R { + try { + return lambda() + } catch (e: TalerConfigError) { + logger.error(e.message) + kotlin.system.exitProcess(1) + } +} diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt index 72f45c3d..14cc1666 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -96,10 +96,10 @@ private fun <R> PgConnection.transaction(lambda: (PgConnection) -> R): R { } } -fun initializeDatabaseTables(dbConfig: String, sqlDir: String) { - logger.info("doing DB initialization, sqldir $sqlDir, dbConfig $dbConfig") - pgDataSource(dbConfig).pgConnection().use { conn -> - val sqlVersioning = File("$sqlDir/versioning.sql").readText() +fun initializeDatabaseTables(cfg: DatabaseConfig) { + logger.info("doing DB initialization, sqldir ${cfg.sqlDir}, dbConnStr ${cfg.dbConnStr}") + pgDataSource(cfg.dbConnStr).pgConnection().use { conn -> + val sqlVersioning = File("${cfg.sqlDir}/versioning.sql").readText() conn.execSQLUpdate(sqlVersioning) val checkStmt = conn.prepareStatement("SELECT count(*) as n FROM _v.patches where patch_name = ?") @@ -115,7 +115,7 @@ fun initializeDatabaseTables(dbConfig: String, sqlDir: String) { continue } - val path = File("$sqlDir/libeufin-bank-$numStr.sql") + val path = File("${cfg.sqlDir}/libeufin-bank-$numStr.sql") if (!path.exists()) { logger.info("path $path doesn't exist anymore, stopping") break @@ -124,14 +124,14 @@ fun initializeDatabaseTables(dbConfig: String, sqlDir: String) { val sqlPatchText = path.readText() conn.execSQLUpdate(sqlPatchText) } - val sqlProcedures = File("$sqlDir/procedures.sql").readText() + val sqlProcedures = File("${cfg.sqlDir}/procedures.sql").readText() conn.execSQLUpdate(sqlProcedures) } } -fun resetDatabaseTables(dbConfig: String, sqlDir: String) { - logger.info("doing DB initialization, sqldir $sqlDir, dbConfig $dbConfig") - pgDataSource(dbConfig).pgConnection().use { conn -> +fun resetDatabaseTables(cfg: DatabaseConfig) { + logger.info("reset DB, sqldir ${cfg.sqlDir}, dbConnStr ${cfg.dbConnStr}") + pgDataSource(cfg.dbConnStr).pgConnection().use { conn -> val count = conn.prepareStatement("SELECT count(*) FROM information_schema.schemata WHERE schema_name='_v'").oneOrNull { it.getInt(1) } ?: 0 @@ -140,12 +140,8 @@ fun resetDatabaseTables(dbConfig: String, sqlDir: String) { return } - val sqlDrop = File("$sqlDir/libeufin-bank-drop.sql").readText() - try { - conn.execSQLUpdate(sqlDrop) - } catch (e: Exception) { - - } + val sqlDrop = File("${cfg.sqlDir}/libeufin-bank-drop.sql").readText() + conn.execSQLUpdate(sqlDrop) // TODO can fail ? } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt index ea4ce211..60eae30f 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -20,9 +20,6 @@ package tech.libeufin.bank -import ConfigSource -import TalerConfig -import TalerConfigError import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.core.subcommands @@ -72,8 +69,6 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Main") const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet. val TOKEN_DEFAULT_DURATION: java.time.Duration = Duration.ofDays(1L) -val BANK_CONFIG_SOURCE = ConfigSource("libeufin-bank", "libeufin-bank") - /** * Application context with the parsed configuration. */ @@ -124,43 +119,7 @@ data class BankApplicationContext( * SPA is located. */ val spaCaptchaURL: String?, -) { - companion object { - /** - * Read the configuration of the bank from a config file. - * Throws an exception if the configuration is malformed. - */ - fun readFromConfig(cfg: TalerConfig): BankApplicationContext { - val currency = cfg.requireString("libeufin-bank", "currency") - val currencySpecification = cfg.sections.find { - it.startsWith("CURRENCY-") && cfg.requireBoolean(it, "enabled") && cfg.requireString(it, "code") == currency - }?.let { - CurrencySpecification( - name = cfg.requireString(it, "name"), - decimal_separator = cfg.requireString(it, "decimal_separator"), - num_fractional_input_digits = cfg.requireNumber(it, "fractional_input_digits"), - num_fractional_normal_digits = cfg.requireNumber(it, "fractional_normal_digits"), - num_fractional_trailing_zero_digits = cfg.requireNumber(it, "fractional_trailing_zero_digits"), - is_currency_name_leading = cfg.requireBoolean(it, "is_currency_name_leading"), - alt_unit_names = Json.decodeFromString(cfg.requireString(it, "alt_unit_names")) - ) - } ?: throw TalerConfigError("missing currency config for $currency") - return BankApplicationContext( - currency = currency, - restrictRegistration = cfg.lookupBoolean("libeufin-bank", "restrict_registration") ?: false, - cashoutCurrency = cfg.lookupString("libeufin-bank", "cashout_currency"), - defaultCustomerDebtLimit = cfg.requireAmount("libeufin-bank", "default_customer_debt_limit", currency), - registrationBonusEnabled = cfg.lookupBoolean("libeufin-bank", "registration_bonus_enabled") ?: false, - registrationBonus = cfg.requireAmount("libeufin-bank", "registration_bonus", currency), - suggestedWithdrawalExchange = cfg.lookupString("libeufin-bank", "suggested_withdrawal_exchange"), - defaultAdminDebtLimit = cfg.requireAmount("libeufin-bank", "default_admin_debt_limit", currency), - spaCaptchaURL = cfg.lookupString("libeufin-bank", "spa_captcha_url"), - restrictAccountDeletion = cfg.lookupBoolean("libeufin-bank", "restrict_account_deletion") ?: true, - currencySpecification = currencySpecification - ) - } - } -} +) /** * This plugin inflates the requests that have "Content-Encoding: deflate" @@ -364,23 +323,6 @@ fun durationFromPretty(s: String): Long { return durationUs } -fun TalerConfig.requireAmount(section: String, option: String, currency: String): TalerAmount { - val amountStr = lookupString(section, option) ?: - throw TalerConfigError("expected amount for section $section, option $option, but config value is empty") - val amount = try { - TalerAmount(amountStr) - } catch (e: Exception) { - throw TalerConfigError("expected amount for section $section, option $option, but amount is malformed") - } - - if (amount.currency != currency) { - throw TalerConfigError( - "expected amount for section $section, option $option, but currency is wrong (got ${amount.currency} expected $currency" - ) - } - return amount -} - class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = "dbinit") { private val configFile by option( "--config", "-c", @@ -399,14 +341,11 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name = } override fun run() { - val config = TalerConfig(BANK_CONFIG_SOURCE) - config.load(this.configFile) - val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config") - val sqlDir = config.requirePath("libeufin-bankdb-postgres", "sql_dir") + val cfg = talerConfig(configFile).loadDbConfig() if (requestReset) { - resetDatabaseTables(dbConnStr, sqlDir) + resetDatabaseTables(cfg) } - initializeDatabaseTables(dbConnStr, sqlDir) + initializeDatabaseTables(cfg) } } @@ -423,24 +362,20 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") } override fun run() { - val config = TalerConfig(BANK_CONFIG_SOURCE) - config.load(this.configFile) - val ctx = BankApplicationContext.readFromConfig(config) - val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config") - logger.info("using database '$dbConnStr'") - val serveMethod = config.requireString("libeufin-bank", "serve") - if (serveMethod.lowercase() != "tcp") { + val cfg = talerConfig(configFile) + val ctx = cfg.loadBankApplicationContext() + val dbCfg = cfg.loadDbConfig() + val serverCfg = cfg.loadServerConfig() + if (serverCfg.method.lowercase() != "tcp") { logger.info("Can only serve libeufin-bank via TCP") exitProcess(1) } - val servePortLong = config.requireNumber("libeufin-bank", "port") - val servePort = servePortLong.toInt() - val db = Database(dbConnStr, ctx.currency) + val db = Database(dbCfg.dbConnStr, ctx.currency) runBlocking { if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper exitProcess(1) } - embeddedServer(Netty, port = servePort) { + embeddedServer(Netty, port = serverCfg.port) { corebankWebApp(db, ctx) }.start(wait = true) } @@ -461,12 +396,10 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { } override fun run() { - val config = TalerConfig(BANK_CONFIG_SOURCE) - config.load(this.configFile) - val ctx = BankApplicationContext.readFromConfig(config) - val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config") - config.requireNumber("libeufin-bank", "port") - val db = Database(dbConnStr, ctx.currency) + val cfg = talerConfig(configFile) + val ctx = cfg.loadBankApplicationContext() + val dbCfg = cfg.loadDbConfig() + val db = Database(dbCfg.dbConnStr, ctx.currency) runBlocking { if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper exitProcess(1) @@ -494,7 +427,7 @@ class BankConfigDump : CliktCommand("Dump the configuration", name = "dump") { } override fun run() { - val config = TalerConfig(BANK_CONFIG_SOURCE) + val config = talerConfig(configFile) println("# install path: ${config.getInstallPath()}") config.load(this.configFile) println(config.stringify()) @@ -516,8 +449,7 @@ class BankConfigPathsub : CliktCommand("Substitute variables in a path", name = } override fun run() { - val config = TalerConfig(BANK_CONFIG_SOURCE) - config.load(this.configFile) + val config = talerConfig(configFile) println(config.pathsub(pathExpr)) } } @@ -544,8 +476,7 @@ class BankConfigGet : CliktCommand("Lookup config value", name = "get") { } override fun run() { - val config = TalerConfig(BANK_CONFIG_SOURCE) - config.load(this.configFile) + val config = talerConfig(configFile) if (isPath) { val res = config.lookupPath(sectionName, optionName) if (res == null) { diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt index a096158c..d878ad2c 100644 --- a/bank/src/test/kotlin/helpers.kt +++ b/bank/src/test/kotlin/helpers.kt @@ -15,14 +15,12 @@ fun setup( conf: String = "test.conf", lambda: suspend (Database, BankApplicationContext) -> Unit ){ - val config = TalerConfig(BANK_CONFIG_SOURCE) - config.load("conf/$conf") - val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config") - val sqlPath = config.requirePath("libeufin-bankdb-postgres", "SQL_DIR") - resetDatabaseTables(dbConnStr, sqlPath) - initializeDatabaseTables(dbConnStr, sqlPath) - val ctx = BankApplicationContext.readFromConfig(config) - Database(dbConnStr, ctx.currency).use { + val config = talerConfig("conf/$conf") + val dbCfg = config.loadDbConfig() + resetDatabaseTables(dbCfg) + initializeDatabaseTables(dbCfg) + val ctx = config.loadBankApplicationContext() + Database(dbCfg.dbConnStr, ctx.currency).use { runBlocking { lambda(it, ctx) } diff --git a/contrib/currencies.conf b/contrib/currencies.conf new file mode 100644 index 00000000..3341a9a7 --- /dev/null +++ b/contrib/currencies.conf @@ -0,0 +1,99 @@ +[currency-euro] +ENABLED = YES +name = "Euro" +code = "EUR" +decimal_separator = "," +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = NO +alt_unit_names = {"0":"€"} + +[currency-swiss-francs] +ENABLED = YES +name = "Swiss Francs" +code = "CHF" +decimal_separator = "." +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = YES +alt_unit_names = {"0":"Fr.","-2":"Rp."} + +[currency-forint] +ENABLED = NO +name = "Hungarian Forint" +code = "HUF" +decimal_separator = "," +fractional_input_digits = 0 +fractional_normal_digits = 0 +fractional_trailing_zero_digits = 0 +is_currency_name_leading = NO +alt_unit_names = {"0":"Ft"} + +[currency-us-dollar] +ENABLED = NO +name = "US Dollar" +code = "USD" +decimal_separator = "." +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = YES +alt_unit_names = {"0":"$"} + +[currency-kudos] +ENABLED = YES +name = "Kudos (Taler Demonstrator)" +code = "KUDOS" +decimal_separator = "," +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = NO +alt_unit_names = {"0":"ク"} + +[currency-testkudos] +ENABLED = YES +name = "Test-kudos (Taler Demonstrator)" +code = "TESTKUDOS" +decimal_separator = "." +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = NO +alt_unit_names = {"0":"テ","3":"kテ","-3":"mテ"} + +[currency-japanese-yen] +ENABLED = NO +name = "Japanese Yen" +code = "JPY" +decimal_separator = "." +fractional_input_digits = 2 +fractional_normal_digits = 0 +fractional_trailing_zero_digits = 2 +is_currency_name_leading = YES +alt_unit_names = {"0":"¥"} + +[currency-bitcoin-mainnet] +ENABLED = NO +name = "Bitcoin (Mainnet)" +code = "BITCOINBTC" +decimal_separator = "." +fractional_input_digits = 8 +fractional_normal_digits = 3 +fractional_trailing_zero_digits = 0 +is_currency_name_leading = NO +alt_unit_names = {"0":"BTC","-3":"mBTC"} + +[currency-ethereum] +ENABLED = NO +name = "WAI-ETHER (Ethereum)" +code = "EthereumWAI" +decimal_separator = "." +fractional_input_digits = 0 +fractional_normal_digits = 0 +fractional_trailing_zero_digits = 0 +is_currency_name_leading = NO +alt_unit_names = {"0":"WAI","3":"KWAI","6":"MWAI","9":"GWAI","12":"Szabo","15":"Finney","18":"Ether","21":"KEther","24":"MEther"} + |