From 6f35a68586680447db6bef301fd7ca20a9d8bcd1 Mon Sep 17 00:00:00 2001 From: eljamm Date: Wed, 4 Sep 2024 10:03:53 +0100 Subject: [PATCH] nixos/libeufin: init module nixos/libeufin: init module nixos/libeufin(nexus): init submodule nixos/libeufin(nexus): refactor service Also added state directory to allow the creation of client ebic keys. nixos/libeufin: review suggestions nixos/libeufin: fix nexus service executable nixos/libeufin: add mkLibeufinModule nixos/libeufin: fix dbinit service not starting for utils, cleanup nixos/libeufin: use mkLibeufinModule for nexus nixos/libeufin: use mkLibeufinModule for bank nixos/libeufin: add initialAccounts, stateDir options nixos/libeufin: refactor to make nexus work, cleanup nixos/libeufin: refactor stateDir, only register accounts on init nixos/libeufin: explicitly specify psql user Sometimes the dbinit service fails to find the user. nixos/libeufin: cleanup stateDir nixos/libeufin: add openFirewall option; install package feat: apply review suggestions Co-authored-by: h7x4 style: format code fix: evaluation errors fix(libeufin): start main services after dbinit The main services can start after their databases have been initialized, it's just that the bank and nexus shouldn't do the initialization at the same time. refactor(libeufin): dbinit script feat: add assertions, remove throw chore: remove unused code feat(libeufin): recfactor dbinit service feat: move libeufin module to services/finance refactor(libeufin): remove configFile option refactor(libeufin): use environment.etc for config file --- nixos/modules/module-list.nix | 3 + .../services/finance/libeufin/bank.nix | 92 ++++++++++ .../services/finance/libeufin/common.nix | 157 ++++++++++++++++++ .../services/finance/libeufin/module.nix | 26 +++ .../services/finance/libeufin/nexus.nix | 131 +++++++++++++++ 5 files changed, 409 insertions(+) create mode 100644 nixos/modules/services/finance/libeufin/bank.nix create mode 100644 nixos/modules/services/finance/libeufin/common.nix create mode 100644 nixos/modules/services/finance/libeufin/module.nix create mode 100644 nixos/modules/services/finance/libeufin/nexus.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 743ae905388bd..d481dc3588910 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -571,6 +571,9 @@ ./services/editors/emacs.nix ./services/editors/haste.nix ./services/editors/infinoted.nix + ./services/finance/libeufin/bank.nix + ./services/finance/libeufin/module.nix + ./services/finance/libeufin/nexus.nix ./services/finance/odoo.nix ./services/finance/taler/exchange.nix ./services/finance/taler/merchant.nix diff --git a/nixos/modules/services/finance/libeufin/bank.nix b/nixos/modules/services/finance/libeufin/bank.nix new file mode 100644 index 0000000000000..77397d25fd528 --- /dev/null +++ b/nixos/modules/services/finance/libeufin/bank.nix @@ -0,0 +1,92 @@ +{ + lib, + config, + options, + ... +}: +{ + imports = [ (import ./common.nix "bank") ]; + + options.services.libeufin.bank = { + initialAccounts = lib.mkOption { + type = lib.types.listOf lib.types.attrs; + description = '' + Accounts to enable before the bank service starts. + + This is mainly needed for the nexus currency conversion + since the exchange's bank account is expected to be already + registered. + + Don't forget to change the account passwords afterwards. + ''; + default = [ ]; + }; + + settings = lib.mkOption { + description = '' + Configuration options for the libeufin bank system config file. + + For a list of all possible options, please see the man page [`libeufin-bank.conf(5)`](https://docs.taler.net/manpages/libeufin-bank.conf.5.html) + ''; + type = lib.types.submodule { + inherit (options.services.libeufin.settings.type.nestedTypes) freeformType; + options = { + libeufin-bank = { + CURRENCY = lib.mkOption { + type = lib.types.str; + description = '' + The currency under which the libeufin-bank should operate. + + This defaults to the GNU taler module's currency for convenience + but if you run libeufin-bank separately from taler, you must set + this yourself. + ''; + }; + PORT = lib.mkOption { + type = lib.types.port; + default = 8082; + description = '' + The port on which libeufin-bank should listen. + ''; + }; + SUGGESTED_WITHDRAWAL_EXCHANGE = lib.mkOption { + type = lib.types.str; + default = "https://exchange.demo.taler.net/"; + description = '' + Exchange that is suggested to wallets when withdrawing. + + Note that, in order for withdrawals to work, your libeufin-bank + must be able to communicate with and send money etc. to the bank + at which the exchange used for withdrawals has its bank account. + + If you also have your own bank and taler exchange network, you + probably want to set one of your exchange's url here instead of + the demo exchange. + + This setting must always be set in order for the Android app to + not crash during the withdrawal process but the exchange to be + used can always be changed in the app. + ''; + }; + }; + libeufin-bankdb-postgres = { + CONFIG = lib.mkOption { + type = lib.types.str; + description = '' + The database connection string for the libeufin-bank database. + ''; + }; + }; + }; + }; + }; + }; + + config = { + services.libeufin.bank.settings.libeufin-bank.CURRENCY = lib.mkIf ( + config.services.taler.enable && (config.services.taler.settings.taler ? CURRENCY) + ) config.services.taler.settings.taler.CURRENCY; + + services.libeufin.bank.settings.libeufin-bankdb-postgres.CONFIG = lib.mkIf config.services.libeufin.bank.createLocalDatabase "postgresql:///libeufin-bank"; + }; +} diff --git a/nixos/modules/services/finance/libeufin/common.nix b/nixos/modules/services/finance/libeufin/common.nix new file mode 100644 index 0000000000000..d5e72b8ad0c5b --- /dev/null +++ b/nixos/modules/services/finance/libeufin/common.nix @@ -0,0 +1,157 @@ +# TODO: create a common module generator for Taler and Libeufin? +libeufinComponent: +{ + lib, + pkgs, + config, + ... +}: +{ + options.services.libeufin.${libeufinComponent} = { + enable = lib.mkEnableOption "libeufin core banking system and web interface"; + package = lib.mkPackageOption pkgs "libeufin" { }; + debug = lib.mkEnableOption "debug logging"; + createLocalDatabase = lib.mkEnableOption "automatic creation of a local postgres database"; + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to open ports in the firewall"; + }; + }; + + config = + let + cfg = cfgMain.${libeufinComponent}; + cfgMain = config.services.libeufin; + + configFile = config.environment.etc."libeufin/libeufin.conf".source; + serviceName = "libeufin-${libeufinComponent}"; + isNexus = libeufinComponent == "nexus"; + + # get database name from config + # TODO: should this always be the same db? In which case, should this be an option directly under `services.libeufin`? + dbName = + lib.removePrefix "postgresql:///" + cfg.settings."libeufin-${libeufinComponent}db-postgres".CONFIG; + + bankPort = cfg.settings."${if isNexus then "nexus-httpd" else "libeufin-bank"}".PORT; + in + lib.mkIf cfg.enable { + services.libeufin.settings = cfg.settings; + + # TODO add system-libeufin.slice? + systemd.services = { + # Main service + "${serviceName}" = { + serviceConfig = { + DynamicUser = true; + ExecStart = + let + args = lib.cli.toGNUCommandLineShell { } { + c = configFile; + L = if cfg.debug then "debug" else null; + }; + in + "${lib.getExe' cfg.package "libeufin-${libeufinComponent}"} serve ${args}"; + Restart = "on-failure"; + RestartSec = "10s"; + }; + requires = [ "libeufin-dbinit.service" ]; + after = [ "libeufin-dbinit.service" ]; + wantedBy = [ "multi-user.target" ]; + }; + + # Database Initialisation + libeufin-dbinit = + let + dbScript = pkgs.writers.writeText "libeufin-db-permissions.sql" '' + GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA libeufin_bank TO "${serviceName}"; + GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA libeufin_nexus TO "${serviceName}"; + GRANT USAGE ON SCHEMA libeufin_bank TO "${serviceName}"; + GRANT USAGE ON SCHEMA libeufin_nexus TO "${serviceName}"; + ''; + + # Accounts to be created after the bank database initialization. + # + # For example, if the bank's currency conversion is enabled, it's + # required that the exchange account is registered before the + # service starts. + initialAccountRegistration = lib.concatMapStringsSep "\n" ( + account: + let + args = lib.cli.toGNUCommandLineShell { } { + c = configFile; + inherit (account) username password name; + payto_uri = "payto://x-taler-bank/bank:${toString bankPort}/${account.username}?receiver-name=${account.name}"; + exchange = lib.toLower account.username == "exchange"; + }; + in + "${lib.getExe' cfg.package "libeufin-bank"} create-account ${args}" + ) cfg.initialAccounts; + + args = lib.cli.toGNUCommandLineShell { } { + c = configFile; + L = if cfg.debug then "debug" else null; + }; + in + { + path = [ config.services.postgresql.package ]; + serviceConfig = { + Type = "oneshot"; + DynamicUser = true; + StateDirectory = "libeufin-dbinit"; + StateDirectoryMode = "0750"; + User = dbName; + }; + script = lib.optionalString cfg.enable '' + ${lib.getExe' cfg.package "libeufin-${libeufinComponent}"} dbinit ${args} + ''; + # Grant DB permissions after schemas have been created + postStart = + '' + psql -U "${dbName}" -f "${dbScript}" + '' + + lib.optionalString ((!isNexus) && (cfg.initialAccounts != [ ])) '' + # only register initial accounts once + if [ ! -e /var/lib/libeufin-dbinit/init ]; then + ${initialAccountRegistration} + + touch /var/lib/libeufin-dbinit/init + echo "Bank initialisation complete" + fi + ''; + requires = lib.optionals cfg.createLocalDatabase [ "postgresql.service" ]; + after = [ "network.target" ] ++ lib.optionals cfg.createLocalDatabase [ "postgresql.service" ]; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ + bankPort + ]; + }; + + environment.systemPackages = [ cfg.package ]; + + services.postgresql = lib.mkIf cfg.createLocalDatabase { + enable = true; + ensureDatabases = [ dbName ]; + ensureUsers = [ + { name = serviceName; } + { + name = dbName; + ensureDBOwnership = true; + } + ]; + }; + + assertions = [ + { + assertion = + cfg.createLocalDatabase || (cfg.settings."libeufin-${libeufinComponent}db-postgres" ? CONFIG); + message = "Libeufin ${libeufinComponent} database is not configured."; + } + ]; + + }; +} diff --git a/nixos/modules/services/finance/libeufin/module.nix b/nixos/modules/services/finance/libeufin/module.nix new file mode 100644 index 0000000000000..1e78e93df3d3c --- /dev/null +++ b/nixos/modules/services/finance/libeufin/module.nix @@ -0,0 +1,26 @@ +{ + lib, + pkgs, + config, + ... +}: + +let + cfg = config.services.libeufin; + settingsFormat = pkgs.formats.ini { }; + configFile = settingsFormat.generate "generated-libeufin.conf" cfg.settings; +in + +{ + options.services.libeufin = { + settings = lib.mkOption { + description = "Global configuration options for the libeufin bank system config file."; + type = lib.types.submodule { freeformType = settingsFormat.type; }; + default = { }; + }; + }; + + config = lib.mkIf (cfg.bank.enable || cfg.nexus.enable) { + environment.etc."libeufin/libeufin.conf".source = configFile; + }; +} diff --git a/nixos/modules/services/finance/libeufin/nexus.nix b/nixos/modules/services/finance/libeufin/nexus.nix new file mode 100644 index 0000000000000..19f877ba6b1b3 --- /dev/null +++ b/nixos/modules/services/finance/libeufin/nexus.nix @@ -0,0 +1,131 @@ +{ + lib, + config, + options, + ... +}: +{ + imports = [ (import ./common.nix "nexus") ]; + + options.services.libeufin.nexus.settings = lib.mkOption { + description = '' + Configuration options for the libeufin nexus config file. + + For a list of all possible options, please see the man page [`libeufin-nexus.conf(5)`](https://docs.taler.net/manpages/libeufin-nexus.conf.5.html) + ''; + type = lib.types.submodule { + inherit (options.services.libeufin.settings.type.nestedTypes) freeformType; + options = { + nexus-ebics = { + # Mandatory configuration values + # https://docs.taler.net/libeufin/nexus-manual.html#setting-up-the-ebics-subscriber + # https://docs.taler.net/libeufin/setup-ebics-at-postfinance.html + CURRENCY = lib.mkOption { + description = "Name of the fiat currency."; + type = lib.types.nonEmptyStr; + example = "CHF"; + }; + HOST_BASE_URL = lib.mkOption { + description = "URL of the EBICS server."; + type = lib.types.nonEmptyStr; + example = "https://ebics.postfinance.ch/ebics/ebics.aspx"; + }; + BANK_DIALECT = lib.mkOption { + description = '' + Name of the following combination: EBICS version and ISO20022 + recommendations that Nexus would honor in the communication with the + bank. + + Currently only the "postfinance" or "gls" value is supported. + ''; + type = lib.types.enum [ + "postfinance" + "gls" + ]; + example = "postfinance"; + }; + HOST_ID = lib.mkOption { + description = "Name of the EBICS host."; + type = lib.types.nonEmptyStr; + example = "PFEBICS"; + }; + USER_ID = lib.mkOption { + description = '' + User ID of the EBICS subscriber. + + This value must be assigned by the bank after having activated a new EBICS subscriber. + ''; + type = lib.types.nonEmptyStr; + example = "PFC00563"; + }; + PARTNER_ID = lib.mkOption { + description = '' + Partner ID of the EBICS subscriber. + + This value must be assigned by the bank after having activated a new EBICS subscriber. + ''; + type = lib.types.nonEmptyStr; + example = "PFC00563"; + }; + IBAN = lib.mkOption { + description = "IBAN of the bank account that is associated with the EBICS subscriber."; + type = lib.types.nonEmptyStr; + example = "CH7789144474425692816"; + }; + BIC = lib.mkOption { + description = "BIC of the bank account that is associated with the EBICS subscriber."; + type = lib.types.nonEmptyStr; + example = "POFICHBEXXX"; + }; + NAME = lib.mkOption { + description = "Legal entity that is associated with the EBICS subscriber."; + type = lib.types.nonEmptyStr; + example = "John Smith S.A."; + }; + BANK_PUBLIC_KEYS_FILE = lib.mkOption { + type = lib.types.path; + default = "/var/lib/libeufin-nexus/bank-ebics-keys.json"; + description = '' + Filesystem location where Nexus should store the bank public keys. + ''; + }; + CLIENT_PRIVATE_KEYS_FILE = lib.mkOption { + type = lib.types.path; + default = "/var/lib/libeufin-nexus/client-ebics-keys.json"; + description = '' + Filesystem location where Nexus should store the subscriber private keys. + ''; + }; + }; + nexus-httpd = { + PORT = lib.mkOption { + type = lib.types.port; + default = 8084; + description = '' + The port on which libeufin-bank should listen. + ''; + }; + }; + libeufin-nexusdb-postgres = { + CONFIG = lib.mkOption { + type = lib.types.str; + description = '' + The database connection string for the libeufin-nexus database. + ''; + }; + }; + }; + }; + }; + + config = + let + cfgMain = config.services.libeufin; + cfg = config.services.libeufin.nexus; + in + lib.mkIf cfg.enable { + services.libeufin.nexus.settings.libeufin-nexusdb-postgres.CONFIG = lib.mkIf ( + cfgMain.bank.enable && cfgMain.bank.createLocalDatabase + ) "postgresql:///libeufin-bank"; + }; +}