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:
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