From 7c51b855f7188c7169252be578e1f61773579442 Mon Sep 17 00:00:00 2001 From: Atemu Date: Thu, 27 Jun 2024 18:47:14 +0200 Subject: [PATCH] nixos/taler: init module basic config set that makes the service at least start add secmod helpers and taler-global runtime dir support for includes taler denominations Only enable services if taler is enabled fix wirewatch service name use correct permissions for database schema The current permissions don't work or aren't enough and cause the wirewatch and closer services to fail. nixos/libeufin: init module libeufin: refactor module libeufin: add main service nixos/taler: configure settings using options Works, but can be refactored further taler: refactor settings options trim settings defaults to the absolutely necessary ones nixos/libeufin: refactor and move to separate dir nixos/libeufin: set defaultText nixos/libeufin: use getExe nixos/libeufin-bank: move to own dir nixos/libeufin: move libeufin related config into its own config file nixos/libeufin/bank: extract dbinitServiceName into var nixos/libeufin: move script to ExecStart nixos/libeufin: fix config file name nixos/taler: refactor config file nixos/taler-exchange: grant delete to taler-exchange-aggregator Would repeatedly attempt to delete in a table where it wasn't allowed to and cause insane spam in the postgres log. nixos/taler/exchange: move exchange-specific options to exchange nixos/taler: move generic taler settings into taler system module nixos/taler: import exchange in module-list.nix nixos/taler-exchange: refactor services group name nixos/taler-exchange: use taler-harness to generate coins The taler-wallet-cli does not have the deployment subcommand anymore, but the docs still say that it should be used to generate the keys. For now, the keys should be generated with taler-harness. nixos/taler-exchange: add option to enable accounts nixos/taler: add missing descriptions nixos/taler(exchange): add description & use getExe' nixos/taler(merchant): init submodule nixos/taler: use correct script for db access nixos/taler: merchant add depositcheck path nixos/taler: review suggestions nixos/taler: make runtimeDir into an option, refactor nixos/taler: init mkTalerModule nixos/taler: use mkTalerModule for exchange nixos/taler: exchange fixups nixos/taler: use mkTalerModule for merchant nixos/taler: improve how dbInit script is created nixos/taler: remove exchange enableAccounts option nixos/taler: explicitly specify psql user Sometimes the dbinit service fails to find the user. nixos/taler: add openFirewall option; install package feat: add assertions, remove throw feat(taler): use module system instead of functions Also: - remove throw from denominateConfig - rename `utils.nix` to `common.nix` feat(taler): refactor modules feat: move taler module to services/finance refactor(exchange): replace throw with assert refactor(exchange,merchant): settings options fix(taler): manpage URLs fix(exchange): public key assert refactor(taler): use configFile feat(taler): include component configs directly Makes services detect config changes better. --- nixos/modules/module-list.nix | 3 + .../modules/services/finance/taler/common.nix | 119 ++++++++++++++ .../services/finance/taler/exchange.nix | 154 ++++++++++++++++++ .../services/finance/taler/merchant.nix | 108 ++++++++++++ .../modules/services/finance/taler/module.nix | 93 +++++++++++ 5 files changed, 477 insertions(+) create mode 100644 nixos/modules/services/finance/taler/common.nix create mode 100644 nixos/modules/services/finance/taler/exchange.nix create mode 100644 nixos/modules/services/finance/taler/merchant.nix create mode 100644 nixos/modules/services/finance/taler/module.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 8fa4b6e9d1107..743ae905388bd 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -572,6 +572,9 @@ ./services/editors/haste.nix ./services/editors/infinoted.nix ./services/finance/odoo.nix + ./services/finance/taler/exchange.nix + ./services/finance/taler/merchant.nix + ./services/finance/taler/module.nix ./services/games/archisteamfarm.nix ./services/games/armagetronad.nix ./services/games/crossfire-server.nix diff --git a/nixos/modules/services/finance/taler/common.nix b/nixos/modules/services/finance/taler/common.nix new file mode 100644 index 0000000000000..426647f8f4764 --- /dev/null +++ b/nixos/modules/services/finance/taler/common.nix @@ -0,0 +1,119 @@ +# TODO: create a common module generator for Taler and Libeufin? +{ + talerComponent ? "", + servicesDB ? [ ], + servicesNoDB ? [ ], + ... +}: +{ + lib, + pkgs, + config, + ... +}: +let + cfg = cfgTaler.${talerComponent}; + cfgTaler = config.services.taler; + + settingsFormat = pkgs.formats.ini { }; + + configFile = config.environment.etc."taler/taler.conf".source; + componentConfigFile = settingsFormat.generate "generated-taler-${talerComponent}.conf" cfg.settings; + + services = servicesDB ++ servicesNoDB; + + dbName = "taler-${talerComponent}-httpd"; + groupName = "taler-${talerComponent}-services"; + + inherit (cfgTaler) runtimeDir; +in +{ + options = { + services.taler.${talerComponent} = { + enable = lib.mkEnableOption "the GNU Taler ${talerComponent}"; + package = lib.mkPackageOption pkgs "taler-${talerComponent}" { }; + # TODO: make option accept multiple debugging levels? + debug = lib.mkEnableOption "debug logging"; + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to open ports in the firewall"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.taler.enable = cfg.enable; + services.taler.includes = [ componentConfigFile ]; + + systemd.services = lib.mergeAttrsList [ + # Main services + (lib.genAttrs (map (n: "taler-${talerComponent}-${n}") services) (name: { + serviceConfig = { + DynamicUser = true; + User = name; + Group = groupName; + ExecStart = toString [ + (lib.getExe' cfg.package name) + "-c ${configFile}" + (lib.optionalString cfg.debug " -L debug") + ]; + RuntimeDirectory = name; + StateDirectory = name; + CacheDirectory = name; + ReadWritePaths = [ runtimeDir ]; + Restart = "always"; + RestartSec = "10s"; + }; + requires = [ "taler-${talerComponent}-dbinit.service" ]; + after = [ "taler-${talerComponent}-dbinit.service" ]; + wantedBy = [ "multi-user.target" ]; # TODO slice? + })) + # Database Initialisation + { + "taler-${talerComponent}-dbinit" = { + path = [ config.services.postgresql.package ]; + serviceConfig = { + Type = "oneshot"; + DynamicUser = true; + User = dbName; + Restart = "on-failure"; + RestartSec = "5s"; + }; + requires = [ "postgresql.service" ]; + after = [ "postgresql.service" ]; + }; + } + ]; + + users.groups.${groupName} = { }; + systemd.tmpfiles.settings = { + "10-taler-${talerComponent}" = { + "${runtimeDir}" = { + d = { + group = groupName; + user = "nobody"; + mode = "070"; + }; + }; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.settings."${talerComponent}".PORT ]; + }; + + environment.systemPackages = [ cfg.package ]; + + services.postgresql = { + enable = true; + ensureDatabases = [ dbName ]; + ensureUsers = map (service: { name = "taler-${talerComponent}-${service}"; }) servicesDB ++ [ + { + name = dbName; + ensureDBOwnership = true; + } + ]; + }; + }; +} diff --git a/nixos/modules/services/finance/taler/exchange.nix b/nixos/modules/services/finance/taler/exchange.nix new file mode 100644 index 0000000000000..16428a43d78dc --- /dev/null +++ b/nixos/modules/services/finance/taler/exchange.nix @@ -0,0 +1,154 @@ +{ + lib, + config, + options, + pkgs, + ... +}: + +let + cfg = cfgTaler.exchange; + cfgTaler = config.services.taler; + + talerComponent = "exchange"; + + # https://docs.taler.net/taler-exchange-manual.html#services-users-groups-and-file-system-hierarchy + servicesDB = [ + "httpd" + "aggregator" + "closer" + "wirewatch" + ]; + + servicesNoDB = [ + "secmod-cs" + "secmod-eddsa" + "secmod-rsa" + ]; +in + +{ + imports = [ + (import ./common.nix { inherit talerComponent servicesDB servicesNoDB; }) + ]; + + options.services.taler.exchange = { + settings = lib.mkOption { + description = '' + Configuration options for the taler exchange config file. + + For a list of all possible options, please see the man page [`taler-exchange.conf(5)`](https://docs.taler.net/manpages/taler-exchange.conf.5.html) + ''; + type = lib.types.submodule { + inherit (options.services.taler.settings.type.nestedTypes) freeformType; + options = { + # TODO: do we want this to be a sub-attribute or only define the exchange set of options here + exchange = { + AML_THRESHOLD = lib.mkOption { + type = lib.types.str; + default = "${cfgTaler.settings.taler.CURRENCY}:1000000"; + defaultText = "1000000 in {option}`CURRENCY`"; + description = "Monthly transaction volume until an account is considered suspicious and flagged for AML review."; + }; + DB = lib.mkOption { + type = lib.types.enum [ "postgres" ]; + default = "postgres"; + description = "Plugin to use for the database."; + }; + MASTER_PUBLIC_KEY = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Used by the exchange to verify information signed by the offline system."; + }; + PORT = lib.mkOption { + type = lib.types.port; + default = 8081; + description = "Port on which the HTTP server listens."; + }; + }; + exchangedb-postgres = { + CONFIG = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "postgres:///taler-exchange-httpd"; + description = "Database connection URI."; + }; + }; + }; + }; + default = { }; + }; + denominationConfig = lib.mkOption { + type = lib.types.lines; + defaultText = "None, you must set this yourself."; + example = '' + [COIN-KUDOS-n1-t1718140083] + VALUE = KUDOS:0.1 + DURATION_WITHDRAW = 7 days + DURATION_SPEND = 2 years + DURATION_LEGAL = 6 years + FEE_WITHDRAW = KUDOS:0 + FEE_DEPOSIT = KUDOS:0.1 + FEE_REFRESH = KUDOS:0 + FEE_REFUND = KUDOS:0 + RSA_KEYSIZE = 2048 + CIPHER = RSA + ''; + description = '' + This option configures the cash denomination for the coins that the exchange offers. + For more information, consult the [upstream docs](https://docs.taler.net/taler-exchange-manual.html#coins-denomination-keys). + + You can either write these manually or you can use the `taler-harness deployment gen-coin-config` + command to generate it. + + Warning: Do not modify existing denominations after deployment. + Please see the upstream docs for how to safely do that. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.settings.exchange.MASTER_PUBLIC_KEY != ""; + message = '' + You must provide `config.services.taler.exchange.settings.exchange.MASTER_PUBLIC_KEY` with the + public part of your master key. + + This will be used by the auditor service to get information about the exchange. + For more information, see https://docs.taler.net/taler-auditor-manual.html#initial-configuration + + To generate this key, you must run `taler-exchange-offline setup`, which will print the public key. + ''; + } + ]; + + services.taler.includes = [ + (pkgs.writers.writeText "exchange-denominations.conf" cfg.denominationConfig) + ]; + + systemd.services.taler-exchange-wirewatch = { + requires = [ "taler-exchange-httpd.service" ]; + after = [ "taler-exchange-httpd.service" ]; + }; + + # Taken from https://docs.taler.net/taler-exchange-manual.html#exchange-database-setup + # TODO: Why does aggregator need DELETE? + systemd.services."taler-${talerComponent}-dbinit".script = + let + deletePerm = name: lib.optionalString (name == "aggregator") ",DELETE"; + dbScript = pkgs.writers.writeText "taler-exchange-db-permissions.sql" ( + lib.pipe servicesDB [ + (map (name: '' + GRANT SELECT,INSERT,UPDATE${deletePerm name} ON ALL TABLES IN SCHEMA exchange TO "taler-exchange-${name}"; + GRANT USAGE ON SCHEMA exchange TO "taler-exchange-${name}"; + '')) + lib.concatStrings + ] + ); + in + '' + ${lib.getExe' cfg.package "taler-exchange-dbinit"} + psql -U taler-exchange-httpd -f ${dbScript} + ''; + }; +} diff --git a/nixos/modules/services/finance/taler/merchant.nix b/nixos/modules/services/finance/taler/merchant.nix new file mode 100644 index 0000000000000..e5a2f2ce5d106 --- /dev/null +++ b/nixos/modules/services/finance/taler/merchant.nix @@ -0,0 +1,108 @@ +{ + lib, + config, + options, + pkgs, + ... +}: +let + cfg = cfgTaler.merchant; + cfgTaler = config.services.taler; + + talerComponent = "merchant"; + + # https://docs.taler.net/taler-merchant-manual.html#launching-the-backend + servicesDB = [ + "httpd" + "webhook" + "wirewatch" + "depositcheck" + "exchange" + ]; +in +{ + imports = [ + (import ./common.nix { inherit talerComponent servicesDB; }) + ]; + + options.services.taler.merchant = { + settings = lib.mkOption { + description = '' + Configuration options for the taler merchant config file. + + For a list of all possible options, please see the man page [`taler-merchant.conf(5)`](https://docs.taler.net/manpages/taler-merchant.conf.5.html) + ''; + type = lib.types.submodule { + inherit (options.services.taler.settings.type.nestedTypes) freeformType; + options = { + merchant = { + DB = lib.mkOption { + type = lib.types.enum [ "postgres" ]; + default = "postgres"; + description = "Plugin to use for the database."; + }; + PORT = lib.mkOption { + type = lib.types.port; + default = 8083; + description = "Port on which the HTTP server listens."; + }; + SERVE = lib.mkOption { + type = lib.types.enum [ + "tcp" + "unix" + ]; + default = "tcp"; + description = '' + Whether the HTTP server should listen on a UNIX domain socket ("unix") or on a TCP socket ("tcp"). + ''; + }; + LEGAL_PRESERVATION = lib.mkOption { + type = lib.types.str; + default = "10 years"; + description = "How long to keep data in the database for tax audits after the transaction has completed."; + }; + }; + merchantdb-postgres = { + CONFIG = lib.mkOption { + type = lib.types.nonEmptyStr; + default = "postgres:///taler-merchant-httpd"; + description = "Database connection URI."; + }; + SQL_DIR = lib.mkOption { + type = lib.types.str; + internal = true; + default = "${cfg.package}/share/taler/sql/merchant/"; + description = "The location for the SQL files to setup the database tables."; + }; + }; + }; + }; + default = { }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.taler-merchant-depositcheck = { + # taler-merchant-depositcheck needs its executable is in the PATH + # NOTE: couldn't use `lib.getExe` to only get that single executable + path = [ cfg.package ]; + }; + + systemd.services."taler-${talerComponent}-dbinit".script = + let + # NOTE: not documented, but is necessary + dbScript = pkgs.writers.writeText "taler-merchant-db-permissions.sql" ( + lib.concatStrings ( + map (name: '' + GRANT SELECT,INSERT,UPDATE,DELETE ON ALL TABLES IN SCHEMA merchant TO "taler-merchant-${name}"; + GRANT USAGE ON SCHEMA merchant TO "taler-merchant-${name}"; + '') servicesDB + ) + ); + in + '' + ${lib.getExe' cfg.package "taler-merchant-dbinit"} + psql -U taler-${talerComponent}-httpd -f ${dbScript} + ''; + }; +} diff --git a/nixos/modules/services/finance/taler/module.nix b/nixos/modules/services/finance/taler/module.nix new file mode 100644 index 0000000000000..844601b19a386 --- /dev/null +++ b/nixos/modules/services/finance/taler/module.nix @@ -0,0 +1,93 @@ +{ + lib, + pkgs, + config, + ... +}: + +let + cfg = config.services.taler; + settingsFormat = pkgs.formats.ini { }; +in + +{ + # TODO turn this into a generic taler-like service thingy? + options.services.taler = { + enable = lib.mkEnableOption "the GNU Taler system" // lib.mkOption { internal = true; }; + includes = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = [ ]; + description = '' + Files to include into the config file using Taler's `@inline@` directive. + + This allows including arbitrary INI files, including imperatively managed ones. + ''; + }; + settings = lib.mkOption { + description = '' + Global configuration options for the taler config file. + + For a list of all possible options, please see the man page [`taler.conf(5)`](https://docs.taler.net/manpages/taler.conf.5.html) + ''; + type = lib.types.submodule { + freeformType = settingsFormat.type; + options = { + taler = { + CURRENCY = lib.mkOption { + type = lib.types.nonEmptyStr; + description = '' + The currency which taler services will operate with. This cannot be changed later. + ''; + }; + CURRENCY_ROUND_UNIT = lib.mkOption { + type = lib.types.str; + default = "${cfg.settings.taler.CURRENCY}:0.01"; + defaultText = lib.literalExpression '' + "''${config.services.taler.settings.taler.CURRENCY}:0.01" + ''; + description = '' + Smallest amount in this currency that can be transferred using the underlying RTGS. + + You should probably not touch this. + ''; + }; + }; + }; + }; + default = { }; + }; + runtimeDir = lib.mkOption { + type = lib.types.str; + default = "/run/taler-system-runtime/"; + description = '' + Runtime directory shared between the taler services. + + Crypto helpers put their sockets here for instance and the httpd + connects to them. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + services.taler.settings.PATHS = { + TALER_DATA_HOME = "\${STATE_DIRECTORY}/"; + TALER_CACHE_HOME = "\${CACHE_DIRECTORY}/"; + TALER_RUNTIME_DIR = cfg.runtimeDir; + }; + + environment.etc."taler/taler.conf".source = + let + includes = pkgs.writers.writeText "includes.conf" ( + lib.concatStringsSep "\n" (map (include: "@inline@ ${include}") cfg.includes) + ); + generatedConfig = settingsFormat.generate "generated-taler.conf" cfg.settings; + in + pkgs.runCommand "taler.conf" { } '' + cat ${includes} > $out + echo >> $out + echo >> $out + cat ${generatedConfig} >> $out + ''; + + }; +}