taler-deployment

Deployment scripts and configuration files
Log | Files | Refs | README

commit 188932bc9a284b5141cec46e41b12195ae7b7931
parent 153c130c59b6b9e2d4171f1e784f07e1eb677cf6
Author: Antoine A <>
Date:   Sat, 22 Feb 2025 14:28:04 +0100

regional: improve config script and add testing config

Diffstat:
Mregional-currency/config.py | 224++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mregional-currency/setup-libeufin.sh | 9+++++++++
2 files changed, 179 insertions(+), 54 deletions(-)

diff --git a/regional-currency/config.py b/regional-currency/config.py @@ -2,13 +2,13 @@ """Python script to ask questions using an interactive prompt""" import base64 +import getpass import os import re import subprocess import urllib.parse import uuid -import getpass -from base64 import b64decode, b64encode +from base64 import b64decode from typing import Callable, Dict, TypeVar import argon2 @@ -46,6 +46,7 @@ def load_conf() -> Dict[str, str]: conf = load_conf() result_conf = {**conf, "CONFIG_LOADED": "y"} + def store_conf(): """Update the configuration file""" content = "" @@ -55,12 +56,14 @@ def store_conf(): with open(CONFIG_FILE, "w") as f: f.write(content) + def add_conf(name: str, value: str): """Update a user configuration value and update the configuration file""" conf[name] = value result_conf[name] = value store_conf() + def run_cmd( cmd: list[str], input: str | None = None, env: Dict[str, str] | None = None ) -> int: @@ -129,33 +132,76 @@ def conf_value( return value +progress = (1, 0) + + +def step(): + global progress + (step, _) = progress + progress = (step + 1, 0) + + +def substep(): + global progress + (step, substep) = progress + progress = (step, substep + 1) + + +def fmt_progress(): + global progress + (step, substep) = progress + if substep == 0: + return f"{step}." + else: + return f"{step}.{substep}." + + def ask( name: str | None, msg: str, default: T | None = None, + example: T | None = None, check: Callable[[str], T | None] = lambda it: it, fmt: Callable[[T], str] = lambda it: str(it), - secret: bool = False + fmt_hint: Callable[[], str] | None = None, + secret: bool = False, ) -> T: """ Prompt the user to configure a value :param name: if present will try to fetch the current value and will store the new value :param msg: the message to prompt the user with :param default: default value to use if no value is obtained + :param example: an example to use as a user hint :param check: check and normalize the value :param fmt: format value for storage + :param fmt_hint: format value for input hint :param secret: hide the input content :return: the configuration value """ + if fmt_hint is None: + if default is not None and not secret: + hint = f" (def. '{default}')" + elif example is not None: + hint = f" (e.g. '{example}')" + else: + hint = "" + else: + hint = fmt_hint() + + end = ": " if "\n" not in msg else "" + + fmt_msg = f"{fmt_progress()} {msg}{hint}{end}" + substep() + def do_ask() -> str | None: # Log the prompt - log.write(msg.encode() + "\n".encode()) + log.write(fmt_msg.encode() + "\n".encode()) # Actual prompt if secret: - raw = getpass.getpass(msg).strip() + raw = getpass.getpass(fmt_msg).strip() else: - print(msg, end='') + print(fmt_msg, end="") raw = input().strip() if raw == "": if default is None: @@ -166,12 +212,35 @@ def ask( return conf_value(name, do_ask, default, check, fmt) -def ask_str(name: str | None, msg: str, default: str | None = None, secret: bool = False) -> str: +def ask_str( + name: str | None, + msg: str, + default: str | None = None, + example: str | None = None, + secret: bool = False, +) -> str: "Prompt the user to configure a string" - return ask(name, msg, default, secret=secret) + return ask(name, msg, default, example=example, secret=secret) + + +def ask_bank_name( + name: str | None, msg: str, test: bool, default: str | None = None +) -> str: + "Prompt the user to configure a BIC" + + def check_bank_name(raw: str) -> str | None: + if test and "test" not in raw.lower(): + print("Test bank name must contain 'Test'") + return None + else: + return raw + return ask(name, msg, default, check=check_bank_name) -def ask_bic(name: str | None, msg: str, default: str | None = None) -> str: + +def ask_bic( + name: str | None, msg: str, default: str | None = None, example: str | None = None +) -> str: "Prompt the user to configure a BIC" def check_bic(raw: str) -> str | None: @@ -182,10 +251,12 @@ def ask_bic(name: str | None, msg: str, default: str | None = None) -> str: else: return raw - return ask(name, msg, default, check_bic) + return ask(name, msg, default, example, check=check_bic) -def ask_iban(name: str | None, msg: str, default: str | None = None) -> str: +def ask_iban( + name: str | None, msg: str, default: str | None = None, example: str | None = None +) -> str: "Prompt the user to configure a IBAN" def check_iban(raw: str) -> str | None: @@ -196,10 +267,12 @@ def ask_iban(name: str | None, msg: str, default: str | None = None) -> str: else: return raw - return ask(name, msg, default, check_iban) + return ask(name, msg, default, example, check=check_iban) -def ask_currency(name: str, msg: str, default: str | None = None) -> str: +def ask_currency( + name: str, msg: str, default: str | None = None, example: str | None = None +) -> str: "Prompt the user to configure a currency name" def check_currency(currency: str) -> str | None: @@ -212,10 +285,12 @@ def ask_currency(name: str, msg: str, default: str | None = None) -> str: return currency return None - return ask(name, msg, default, check_currency) + return ask(name, msg, default, example, check=check_currency) -def ask_host(name: str, msg: str, default: str | None = None) -> str: +def ask_host( + name: str, msg: str, default: str | None = None, example: str | None = None +) -> str: "Prompt the user to configure the installation hostname" def check_host(host: str) -> str | None: @@ -227,7 +302,7 @@ def ask_host(name: str, msg: str, default: str | None = None) -> str: else: return None - return ask(name, msg, default, check_host) + return ask(name, msg, default, example, check=check_host) def ask_terms(name: str, msg: str, kind: str) -> str: @@ -252,7 +327,7 @@ def ask_terms(name: str, msg: str, kind: str) -> str: else: return path - return ask(name, tos_msg, None, check_file) + return ask(name, tos_msg, check=check_file) def ask_yes_no(name: str | None, msg: str, default: bool | None = None) -> bool: @@ -268,7 +343,22 @@ def ask_yes_no(name: str | None, msg: str, default: bool | None = None) -> bool: print("Expected 'y' or 'n'") return None - return ask(name, msg, default, check_yes_no, lambda it: "y" if it else "n") + def fmt_yes_no_default() -> str: + if default is None: + return " (y/n)" + elif default is True: + return " (Y/n)" + else: + return " (y/N)" + + return ask( + name, + msg, + default, + check=check_yes_no, + fmt=lambda it: "y" if it else "n", + fmt_hint=fmt_yes_no_default, + ) # ----- Crypto ----- # @@ -281,14 +371,14 @@ def ask_config_password() -> str: passwd = None if hash is not None: while True: - passwd = ask_str(None, "Enter the config password : ", secret=True) + passwd = ask_str(None, "Enter the config password", secret=True) try: ph.verify(hash, passwd) break except argon2.exceptions.VerifyMismatchError: print("invalid password") else: - passwd = ask_str(None, "1.1 Choose a config password : ", secret=True) + passwd = ask_str(None, "Choose a config password", secret=True) if hash is None or ph.check_needs_rehash(hash): add_conf("CONFIG_PASSWORD", ph.hash(passwd)) @@ -301,7 +391,7 @@ def ask_secret( ) -> str: "Prompt the user to configure a string stored encryped using pbkdf2_sha512 and chacha20_poly1305" if passwd is None: - return ask_str(name, msg, default) + return ask_str(name, msg, default, secret=True) else: raw = conf.get(name) plaintext = None @@ -326,7 +416,7 @@ def ask_secret( f"$pbkdf2_sha512_chacha20_poly1305$1000000${base64.b64encode(salt).decode()}${base64.b64encode(cipher.nonce).decode()}${base64.b64encode(tag).decode()}${base64.b64encode(ciphertext).decode()}", ) else: - plaintext = ask_str(None, msg, default, True) + plaintext = ask_str(None, msg, default, secret=True) salt = get_random_bytes(16) key = PBKDF2(passwd, salt, 32, count=1000000, hmac_hash_module=SHA512) cipher = ChaCha20_Poly1305.new(key=key) @@ -346,60 +436,79 @@ config_passwd = ( ask_config_password() if ask_yes_no( "DO_CONFIG_ENCRYPTION", - "1. Do you want to encrypt sensitive config values (Y/n): ", + "Do you want to encrypt sensitive config values", True, ) else None ) + +step() ask_currency( "CURRENCY", - "2. Enter the name of the regional currency (e.g. 'NETZBON'): ", - "NETZBON", + "Enter the name of the regional currency", + default="NETZBON", ) + +step() do_conversion = ask_yes_no( "DO_CONVERSION", - "3. Do you want setup regional currency conversion to fiat currency (Y/n): ", + "Do you want setup regional currency conversion to fiat currency", True, ) if do_conversion: ask_currency( "FIAT_CURRENCY", - "3.1. Enter the name of the fiat currency (e.g. 'CHF'): ", - "CHF", + "Enter the name of the fiat currency", + default="CHF", ) ask_str( - "FIAT_BANK_NAME", - "3.2. Enter the name of your fiat bank (e.g. POSTFINANCE AG): ", + "FIAT_BANK_NAME", "Enter the name of your fiat bank", example="POSTFINANCE AG" ) iban = ask_iban( "FIAT_ACCOUNT_IBAN", - "3.3. Enter the IBAN of your fiat bank account (e.g. 'CH7789144474425692816'): ", + "Enter the IBAN of your fiat bank account", + example="CH7789144474425692816", ) bic = ask_bic( "FIAT_ACCOUNT_BIC", - "3.4. Enter the BIC of your fiat bank account (e.g. 'POFICHBEXXX'): ", + "Enter the BIC of your fiat bank account", + example="POFICHBEXXX", ) name = ask_str( - "FIAT_ACCOUNT_NAME", "3.5. Enter the legal name of your fiat bank account: " + "FIAT_ACCOUNT_NAME", "Enter the legal name of your fiat bank account" ) params = urllib.parse.urlencode({"receiver-name": name}) add_conf("CONVERSION_PAYTO", f"payto://iban/{bic}/{iban}?{params}") -bank_name = ask_str( + +step() +do_test = ask_yes_no( + "DO_TEST", + "Do you want setup a testing deployment", + False, +) + +step() +bank_name = ask_bank_name( "BANK_NAME", - "4. Enter the human-readable name of the bank (e.g. 'Taler Bank'): ", - "Taler Bank", + "Enter the human-readable name of the bank", + test=do_test, + default="Taler Bank" if not do_test else "Taler Test Bank", ) -ask_host("DOMAIN_NAME", "5. Enter the domain name (e.g. 'example.com'): ") -if ask_yes_no("ENABLE_TLS", "6. Setup TLS using Let's Encrypt? (Y/n): ", True): - ask_str("TLS_EMAIL", "6.1. Enter an email address for Let's Encrypt: ") + +step() +ask_host("DOMAIN_NAME", "Enter the domain name", example="example.com") + +step() +if ask_yes_no("ENABLE_TLS", "Setup TLS using Let's Encrypt?", True): + ask_str("TLS_EMAIL", "Enter an email address for Let's Encrypt") def ask_tos(): print( - "6.2. Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf." + "Please read the Terms of Service at https://letsencrypt.org/documents/LE-SA-v1.3-September-21-2022.pdf." ) if not ask_yes_no( None, - "6.2. You must agree in order to register with the ACME server. Do you agree? (y/n): ", + "You must agree in order to register with the ACME server. Do you agree?", False, ): print("You must agree in order to register with the ACME server") @@ -416,18 +525,18 @@ add_conf( "DO_OFFLINE", "y" ) # TODO support offline setup again when the documentation is ready +step() if ask_yes_no( "DO_TELESIGN", - "7. Setup SMS two-factor authentication using Telesign https://www.telesign.com? (Y/n): ", + "Setup SMS two-factor authentication using Telesign https://www.telesign.com?", True, ): def ask_telesign(): - customer_id = ask_str(None, "7.1. Enter your Telesign Customer ID: ") - api_key = ask_str(None, "7.2. Enter your Telesign API Key: ") + customer_id = ask_str(None, "Enter your Telesign Customer ID") + api_key = ask_str(None, "Enter your Telesign API Key") phone_number = ask_str( - None, - "6.3. Enter a phone number to test your API key (e.g. '+447911123456'): ", + None, "Enter a phone number to test your API key", example="+447911123456" ) auth_token = base64.b64encode(f"{customer_id}:{api_key}".encode()).decode() if not try_cmd( @@ -439,7 +548,7 @@ if ask_yes_no( "Failed to send an SMS using Telesign API, check your credentials and phone number" ) return None - code = ask_str(None, f"7.4. Enter the code received by {phone_number} : ") + code = ask_str(None, f"Enter the code received by {phone_number}") if code != "12345" and code != "T-12345": print( f"Wrong code got '{code}' expected '12345', check your credentials and phone number" @@ -448,34 +557,41 @@ if ask_yes_no( return auth_token conf_value("TELESIGN_AUTH_TOKEN", ask_telesign) -generated_password= str(uuid.uuid4()) + +step() +generated_password = str(uuid.uuid4()) admin_password = ask_secret( "BANK_ADMIN_PASSWORD", - "8. Enter the admin password for the bank (or press enter to autogenerate password): ", + "Enter the admin password for the bank (or press enter to autogenerate password)", config_passwd, generated_password, ) -add_conf("BANK_ADMIN_PASSWORD_GENERATED", "y" if generated_password==admin_password else "n") +add_conf( + "BANK_ADMIN_PASSWORD_GENERATED", + "y" if generated_password == admin_password else "n", +) +step() if ask_yes_no( "DO_EXCHANGE_TERMS", - "9. Do you wish to configure terms of service (ToS) for the exchange? (Y/n): ", + "Do you wish to configure terms of service (ToS) for the exchange?", True, ): ask_terms( "EXCHANGE_TERMS_FILE", - "9.1. Enter the filename of the ToS. Some available options are:\n", + "Enter the filename of the ToS. Some available options are:\n", "-tos-", ) +step() if ask_yes_no( "DO_EXCHANGE_PRIVACY", - "10. Do you wish to configure a privacy policy for the exchange? (Y/n): ", + "Do you wish to configure a privacy policy for the exchange?", True, ): ask_terms( "EXCHANGE_PRIVACY_FILE", - "10.1. Enter the filename of the privacy policy. Some available options are:\n", + "Enter the filename of the privacy policy. Some available options are:\n", "-pp-", ) diff --git a/regional-currency/setup-libeufin.sh b/regional-currency/setup-libeufin.sh @@ -50,6 +50,15 @@ ALLOW_EDIT_CASHOUT_PAYTO_URI=yes EOF fi +if test ${DO_TEST} == y; then + cat >>/etc/libeufin/libeufin-bank.conf <<EOF +ALLOW_REGISTRATION = yes +ALLOW_ACCOUNT_DELETION = yes +ALLOW_EDIT_NAME = yes +DEFAULT_DEBT_LIMIT = ${CURRENCY}:100 +EOF +EOF + if test -n "${TELESIGN_AUTH_TOKEN:-}"; then cat >>/etc/libeufin/libeufin-bank.conf <<EOF TAN_SMS=libeufin-tan-sms.sh