commit f6387885d49a0261323dc1d8653944624453b81a
parent 91001dd6f624dd82bed78d0a48aba48cef7b2e8e
Author: Florian Dold <florian@dold.me>
Date: Thu, 30 Oct 2025 11:28:00 +0100
remote self-provision support
Now supported directly by the merchant
Diffstat:
5 files changed, 3 insertions(+), 302 deletions(-)
diff --git a/debian/rules b/debian/rules
@@ -40,7 +40,6 @@ override_dh_installsystemd:
dh_installsystemd -ptaler-merchant-demos --name=taler-demo-landing --no-start --no-enable
dh_installsystemd -ptaler-merchant-demos --name=taler-demo-blog --no-start --no-enable
dh_installsystemd -ptaler-merchant-demos --name=taler-demo-donations --no-start --no-enable
- dh_installsystemd -ptaler-merchant-demos --name=taler-demo-provision --no-start --no-enable
# final invocation to generate daemon reload
dh_installsystemd
diff --git a/debian/taler-merchant-demos.taler-demo-provision.service b/debian/taler-merchant-demos.taler-demo-provision.service
@@ -1,12 +0,0 @@
-[Unit]
-Description=Taler Demo Merchant Self-Provisioning Service
-
-[Service]
-User=taler-merchant-demos
-ExecStart=/usr/bin/taler-merchant-demos -c /etc/taler/taler-merchant-frontends.conf provision
-EnvironmentFile=/etc/taler/taler-merchant-frontends.env
-Restart=on-failure
-RestartSec=1s
-
-[Install]
-WantedBy=multi-user.target
diff --git a/talermerchantdemos/cli.py b/talermerchantdemos/cli.py
@@ -100,12 +100,12 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication):
)
@click.argument("which-shop")
def demos(config_filename, http_port, which_shop):
- """WHICH_SHOP is one of: blog, donations, provision, or landing."""
+ """WHICH_SHOP is one of: blog, donations, or landing."""
logging.basicConfig(level=logging.INFO)
- if which_shop not in ["blog", "donations", "provision", "landing"]:
- print("Please use a valid shop name: blog, donations, provision, landing.")
+ if which_shop not in ["blog", "donations", "landing"]:
+ print("Please use a valid shop name: blog, donations, landing.")
sys.exit(1)
config = TalerConfig.from_file(config_filename)
options = {
diff --git a/talermerchantdemos/provision/__init__.py b/talermerchantdemos/provision/__init__.py
@@ -1,3 +0,0 @@
-from talermerchantdemos.provision.provision import app
-
-__all__ = ["app"]
diff --git a/talermerchantdemos/provision/provision.py b/talermerchantdemos/provision/provision.py
@@ -1,283 +0,0 @@
-##
-# This file is part of GNU TALER.
-# Copyright (C) 2024 Taler Systems SA
-#
-# TALER is free software; you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free Software
-# Foundation; either version 2.1, or (at your option) any later version.
-#
-# TALER 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 Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License along with
-# GNU TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
-#
-# @author Özgür Kesim
-# @brief Implementation of a merchant self-provision service
-
-import base64
-import logging
-import flask
-from flask import request, url_for
-from flask_babel import Babel
-from flask_babel import gettext
-from werkzeug.middleware.proxy_fix import ProxyFix
-import os
-import babel
-from datetime import datetime
-import re
-import hashlib
-import struct
-import subprocess
-from ..httpcommon import backend_post, backend_get, make_utility_processor
-from ..appconfig import load_taler_config
-import sys
-
-if not sys.version_info.major == 3 and sys.version_info.minor >= 6:
- print("Python 3.6 or higher is required.")
- print(
- "You are using Python {}.{}.".format(
- sys.version_info.major, sys.version_info.minor
- )
- )
- sys.exit(1)
-
-logger = logging.getLogger(__name__)
-
-app = flask.Flask(__name__,
- template_folder="../templates",
- static_folder="../static",
- static_url_path="/static")
-app.wsgi_app = ProxyFix(app.wsgi_app, x_host=1)
-app.debug = True
-app.secret_key = base64.b64encode(os.urandom(64)).decode("utf-8")
-app.config.from_object(__name__)
-
-# The path prefix for the provision system, without the leading or trailing "/"
-provision_prefix=os.environ.get("TALER_ENV_URL_MERCHANT_PROVISION_PREFIX", "provision")
-
-# We need a special local handler for this app,
-# as our path's have the prefix {provision_prefix}
-def get_locale():
- parts = request.path.split("/", 3)
- if 3 >= len(parts):
- # Totally unexpected path format, do not localize
- return "en"
- if parts[1] == f"{provision_prefix}":
- lang = parts[2]
- else:
- lang = parts[1]
-
- # Sanity check on the language code.
- try:
- babel.core.Locale.parse(lang)
- except Exception as err:
- # Not a locale, default to english.
- logger.error(f"language {lang} did not parse, default to english")
- return "en"
- if lang == "static":
- # Static resource, not a language indicator.
- # Do not localize then.
- return "en"
- return lang
-
-BABEL_TRANSLATION_DIRECTORIES = "../translations"
-babbel = Babel(app)
-babbel.localeselector(get_locale)
-
-config = load_taler_config()
-
-CURRENCY = config["taler"]["currency"].value_string(required=True)
-
-backend_urls = {}
-backend_apikeys = {}
-timeouts = {}
-
-timeouts["init"] = config["frontend-demo-provision"][f"timeout_init"].value_string(required=True)
-timeouts["idle"] = config["frontend-demo-provision"][f"timeout_idle"].value_string(required=True)
-
-def add_backend(name):
- backend_urls[name] = config["frontend-demo-provision"][f"backend_url_{name}"].value_string(required=True)
- backend_apikeys[name] = config["frontend-demo-provision"][f"backend_apikey_{name}"].value_string(required=True)
-
-add_backend("merchant")
-add_backend("bank")
-
-
-
-logger.info("Using translations from:" + ":".join(list(babbel.translation_directories)))
-logger.info("currency: " + CURRENCY)
-translations = [str(translation) for translation in babbel.list_translations()]
-if not "en" in translations:
- translations.append("en")
-logger.info(
- "Operating with the following translations available: " + " ".join(translations)
-)
-
-# Add context processor that will make additional variables
-# and functions available in the template.
-# Overwrite the "getlang" helper from httpcommon with the
-# specific one for provision.
-app.context_processor(
- make_utility_processor(
- "provision", os.environ.get("TALER_ENV_URL_MERCHANT_PROVISION"),
- dict(getlang=get_locale)
- )
-)
-
-
-##
-# Return a error response to the client.
-#
-# @param abort_status_code status code to return along the response.
-# @param params _kw_ arguments to passed verbatim to the templating engine.
-def err_abort(abort_status_code, **params):
- t = flask.render_template("provision-error.html.j2", **params)
- flask.abort(flask.make_response(t, abort_status_code))
-
-
-##
-# Issue a GET request to the backend.
-#
-# @param endpoint the backend endpoint where to issue the request.
-# @param params (dict type of) URL parameters to append to the request.
-# @return the JSON response from the backend, or a error response
-# if something unexpected happens.
-def backend_instanced_get(instance, endpoint, params):
- return backend_get(backend_urls[instance], endpoint, params, auth_token=backend_apikeys[instance])
-
-
-##
-# POST a request to the backend, and return a error
-# response if any error occurs.
-#
-# @param endpoint the backend endpoint where to POST
-# this request.
-# @param json the POST's body.
-# @return the backend response (JSON format).
-def backend_instanced_post(instance, endpoint, json):
- return backend_post(backend_urls[instance], endpoint, json, auth_token=backend_apikeys[instance])
-
-
-##
-# "Fallback" exception handler to capture all the unmanaged errors.
-#
-# @param e the Exception object, currently unused.
-# @return flask-native response object carrying the error message
-# (and execution stack!).
-@app.errorhandler(Exception)
-def internal_error(e):
- t = flask.render_template(
- "provision-error.html.j2",
- page_title=gettext("GNU Taler Demo: Error"),
- message=str(e),
- )
- flask.abort(flask.make_response(t, 500))
-
-
-##
-# Serve the main index page, redirecting to /<lang>/
-#
-# @return response object of the index page.
-@app.route(f"/{provision_prefix}/")
-def index():
- default = "en"
- target = flask.request.accept_languages.best_match(translations, default)
- return flask.redirect(url_for("index") + "/"+ target + "/", code=302)
-
-
-##
-# Serve the main index page.
-#
-# @return response object of the index page.
-@app.route(f"/{provision_prefix}/<lang>/")
-def start(lang):
- # get_locale defaults to english, hence the
- # condition below happens only when lang is
- # wrong or unsupported, respond 404.
- if lang != get_locale():
- err_abort(
- 404,
- message=f"Language {lang} not found",
- )
-
- return flask.render_template(
- "provision-index.html.j2",
- page_title=gettext("GNU Taler Demo: Provision"),
- merchant_currency=CURRENCY,
- merchant_url=backend_urls["merchant"],
- bank_url=backend_urls["bank"],
- )
-
-
-# Acceptable merchant names must match this regular expression
-allowed = re.compile("^[a-zA-Z]([a-zA-Z0-9_. -]+)[a-zA-Z0-9][.]?$")
-
-##
-# Handle the "/create" request
-# and create a merchant instance and bank account.
-#
-# @return response object for the /provision page.
-@app.route(f"/{provision_prefix}/<lang>/create", methods=["POST"])
-def create(lang):
- fullname = flask.request.form.get("fullname")
- fullname = fullname.strip(' \t\n\r')
- if not fullname:
- return err_abort(400, message=gettext("Full name required."))
- if not allowed.match(fullname):
- return err_abort(400, message=gettext("Full name not acceptable."))
-
- # Only create an merchant with the same name every 15 minute
- n = datetime.now()
- ts = datetime(n.year, n.month, n.day, n.hour, n.minute % 15).timestamp()
-
- m = hashlib.sha256()
- m.update(fullname.encode('utf-8'))
- m.update(struct.pack('d', ts))
- hash = m.hexdigest()
-
- merchant_id = "merchant-"+hash[:8]
- access_token = hash[8:20]
-
- ret = subprocess.run(["taler-harness",
- "deployment",
- "provision-bank-and-merchant",
- "--legal-name={n}".format(n=fullname),
- "--id={id}".format(id=merchant_id),
- "--password={pw}".format(pw=access_token),
- "--merchant-management-token={t}".format(t=backend_apikeys["merchant"]),
- "--bank-admin-token={t}".format(t=backend_apikeys["bank"]),
- backend_urls["merchant"],
- backend_urls["bank"],
- ], capture_output=True)
-
- if ret.returncode != 0:
- logger.error("taler-harness returned {d},\nstdout:>>>>{o}<<<<\nstderr:>>>>{e}<<<<<\n"
- .format(d=ret.returncode,o=ret.stdout.decode(),e=ret.stderr.decode()))
- return internal_error("Internal error, couldn't create instance. Soooo sorry! 🤷")
-
-
- logger.debug("taler-harness output:>>>>{o}<<<<)".format(o=ret.stdout.decode()))
- logger.info("merchant instance {id} created with hash: {hash}".format(id=merchant_id,hash=hash))
- return flask.render_template(
- "provision-done.html.j2",
- page_title=gettext("GNU Taler Demo: Self-Provision"),
- fullname=fullname,
- merchant_id=merchant_id,
- access_token=access_token,
- bank_url=backend_urls["bank"],
- merchant_url=backend_urls["merchant"],
- timeout_init=timeouts["init"],
- timeout_idle=timeouts["idle"],
- currency=CURRENCY,
- )
-
-
-@app.errorhandler(404)
-def handler(e):
- return flask.render_template(
- "provision-error.html.j2",
- page_title=gettext("GNU Taler Demo: Provision Error"),
- message=gettext("Page not found"),
- )