gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated (0f3604e -> ceab4b8)


From: gnunet
Subject: [libeufin] branch master updated (0f3604e -> ceab4b8)
Date: Wed, 20 Jan 2021 20:32:33 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a change to branch master
in repository libeufin.

    from 0f3604e  address Sandbox compile-warnings
     new 7a36c35  remove obsolete script
     new ccf1edf  remove CLI's README, as it's outdated and superseded by 
docs.git
     new ceab4b8  rudimentary permissions, code cleanup

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .idea/dictionaries/dold.xml                        |   3 +
 .idea/inspectionProfiles/Project_Default.xml       |   1 +
 build.gradle                                       |   2 +-
 cli/README                                         |  78 -------
 cli/bin/libeufin-cli                               | 224 +++++++++++++++++++--
 cli/setup-template.sh                              |   6 +-
 nexus/build.gradle                                 |   5 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt  | 116 +++++++++++
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |  84 +++++---
 nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt  |   1 -
 nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt |  65 +++---
 .../main/kotlin/tech/libeufin/nexus/server/JSON.kt |  49 +++--
 .../tech/libeufin/nexus/server/NexusServer.kt      | 206 ++++++++++---------
 .../nexus/server/RequestBodyDecompression.kt       |  47 +++++
 nexus/src/test/script/prepare_subscriber.sh        |  78 -------
 util/src/main/kotlin/Errors.kt                     |   2 +-
 16 files changed, 604 insertions(+), 363 deletions(-)
 delete mode 100644 cli/README
 create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
 create mode 100644 
nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
 delete mode 100755 nexus/src/test/script/prepare_subscriber.sh

diff --git a/.idea/dictionaries/dold.xml b/.idea/dictionaries/dold.xml
index ce7b473..12a9811 100644
--- a/.idea/dictionaries/dold.xml
+++ b/.idea/dictionaries/dold.xml
@@ -8,6 +8,7 @@
       <w>cronspec</w>
       <w>dbit</w>
       <w>ebics</w>
+      <w>gnunet</w>
       <w>iban</w>
       <w>infos</w>
       <w>libeufin</w>
@@ -15,6 +16,8 @@
       <w>pdng</w>
       <w>servicer</w>
       <w>sqlite</w>
+      <w>taler</w>
+      <w>wtid</w>
     </words>
   </dictionary>
 </component>
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml 
b/.idea/inspectionProfiles/Project_Default.xml
index 030f244..c29fcc6 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,6 +1,7 @@
 <component name="InspectionProjectProfileManager">
   <profile version="1.0">
     <option name="myName" value="Project Default" />
+    <inspection_tool class="FoldInitializerAndIfToElvis" enabled="false" 
level="INFO" enabled_by_default="false" />
     <inspection_tool class="JsonStandardCompliance" enabled="true" 
level="ERROR" enabled_by_default="true">
       <option name="myWarnAboutComments" value="false" />
     </inspection_tool>
diff --git a/build.gradle b/build.gradle
index ca82194..a4b7427 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,5 @@
 plugins {
-    id 'org.jetbrains.kotlin.jvm' version '1.3.72'
+    id 'org.jetbrains.kotlin.jvm' version '1.4.30-RC'
     id 'idea'
 }
 
diff --git a/cli/README b/cli/README
deleted file mode 100644
index 89f7e0e..0000000
--- a/cli/README
+++ /dev/null
@@ -1,78 +0,0 @@
-0.  Prerequisites.
-==================
-
-Before being able to reach your bank via EBICS, please
-make sure that you activated your 'subscriber', and obtained
-the credentials from your bank!
-
-1.  How to launch the Nexus.
-============================
-From the top directory of this Git repository (on a GNU/Linux system):
-    
-    ./gradlew nexus:run
-
-
-2.  How to use the CLI to request your transactions history.
-============================================================
-
-1.  Once you obtained your credentials, you need to "store"
-  them into the Nexus.  After launching the Nexus (see #1),
-  give the command:
-    
-    libeufin-cli ebics new-subscriber \
-      --account-id=<mnemonic-token-you-choose> \
-      --ebics-url=<URL serving EBICS requests, your bank should have notified 
you one> \
-      --user-id=<EBICS specific user id, see your credentials> \
-      --partner-id=<EBICS specific partner id, see your credentials> \
-      --host-id=<EBICS specific token identifying the EBICS server, see your 
credentials> \
-      $NEXUS_BASE_URL
-
-2.  To upload your keys to the bank, and download the bank's.
-    
-    libeufin-cli ebics prepare 
--account-id=<mnemonic-token-you-chose-at-step-1>
-
-
-3 (recommended).  To get a backup of your EBICS keys:
-    
-    libeufin ebics backup \
-        --account-id=<mnemonic-token-you-chose-at-step-1> \
-        --output-file=<path to where to store the backup>
-
-5.  You can now ask for the transactions history concerning your
-  EBICS subscriber.
-    
-    libeufin ebics c52 --account-id=<mnemonic-token-you-chose-at-step-1>
-
-
-3.  Instructions to issue a payment instruction.
-================================================
-
-1.  Link your bank accounts to your EBICS subscriber.
-
-    libeufin-cli ebics fetch-accounts \
-      --account-id=<mnemonic-token-you-chose-at-step-1-of-2> \
-      --prepare \ # shortcut to upload your keys at the bank, and download the 
bank's
-      $NEXUS_BASE_URL
-
-2.  To see all of your bank accounts that are known to the Nexus.
-
-    libeufin-cli ebics bank-accounts \
-      --account-id=<mnemonic-token-you-chose-at-step-1-of-2> \
-      $NEXUS_BASE_URL
-
-3.  Prepare the payment.
-
-    libeufin-cli ebics prepare-payment \
-      --account-id=<mnemonic-token-you-chose-at-step-1-of-2> \
-      --creditor-iban=<IBAN from the bank account that is receiving the 
payment> \
-      --creditor-bic=<BIC from the bank account that is receiving the payment> 
\
-      --creditor-name=<real name of the legal entity associated with the 
creditor IBAN> \
-      --subject=<subject line associated with the money transfer> \
-      --sum=<amount of money to transfer, in the form X[.YY] (no currency 
specified.  Always EUR)> \
-      $NEXUS_BASE_URL
-
-4.  If the previous step succeeded, then the Nexus can be triggered to process 
the
-    pending payment(s), *regardless* of which customer prepared them.  This 
step will
-    be automated in the future, and only needed now to help debugging.
-
-    libeufin-cli ebics execute-payments $NEXUS_BASE_URL
diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli
index 908a6ff..5e89e1e 100755
--- a/cli/bin/libeufin-cli
+++ b/cli/bin/libeufin-cli
@@ -11,25 +11,21 @@ from requests import post, get, auth, delete
 from urllib.parse import urljoin
 from getpass import getpass
 
+
 def tell_user(resp, withsuccess=False):
     if resp.status_code == 200 and not withsuccess:
         return
     print(resp.content.decode("utf-8"))
 
+
+# FIXME: deprecate this in favor of NexusContext
 def fetch_env():
     if "--help" in sys.argv:
         return []
     try:
-        nexus_base_url = os.environ.get("LIBEUFIN_NEXUS_URL")
-        if not nexus_base_url:
-            # compat, should eventually be removed
-            nexus_base_url = os.environ["NEXUS_BASE_URL"]
-        nexus_username = os.environ.get("LIBEUFIN_NEXUS_USERNAME")
-        if not nexus_username:
-            nexus_username = os.environ["NEXUS_USERNAME"]
-        nexus_password = os.environ.get("LIBEUFIN_NEXUS_PASSWORD")
-        if not nexus_password:
-            nexus_password = os.environ["NEXUS_PASSWORD"]
+        nexus_base_url = os.environ["LIBEUFIN_NEXUS_URL"]
+        nexus_username = os.environ["LIBEUFIN_NEXUS_USERNAME"]
+        nexus_password = os.environ["LIBEUFIN_NEXUS_PASSWORD"]
     except KeyError:
         print(
             "Please ensure that NEXUS_BASE_URL,"
@@ -40,6 +36,7 @@ def fetch_env():
     return nexus_base_url, nexus_username, nexus_password
 
 
+# FIXME: deprecate this in favor of NexusContext
 class NexusAccess:
     def __init__(self, nexus_base_url=None, username=None, password=None):
         self.nexus_base_url = nexus_base_url
@@ -65,6 +62,132 @@ def connections(ctx):
     ctx.obj = NexusAccess(*fetch_env())
 
 
+@cli.group()
+@click.pass_context
+def users(ctx):
+    ctx.obj = NexusContext()
+
+@cli.group()
+@click.pass_context
+def permissions(ctx):
+    ctx.obj = NexusContext()
+
+@users.command("list", help="List users")
+@click.pass_obj
+def list_users(obj):
+    url = urljoin(obj.nexus_base_url, f"/users")
+    try:
+        resp = get(url, auth=auth.HTTPBasicAuth(obj.nexus_username, 
obj.nexus_password))
+    except Exception as e:
+        print(e)
+        print("Could not reach nexus at " + url)
+        exit(1)
+
+    print(resp.content.decode("utf-8"))
+
+
+@users.command("create", help="Create a new user")
+@click.argument("username")
+@click.option(
+    "--password",
+    help="Provide password instead of prompting interactively.",
+    prompt=True,
+    hide_input=True,
+    confirmation_prompt=True,
+)
+@click.pass_obj
+def create_user(obj, username, password):
+    url = urljoin(obj.nexus_base_url, f"/users")
+    try:
+        body = dict(
+            username=username,
+            password=password,
+        )
+        resp = post(
+            url,
+            json=body,
+            auth=auth.HTTPBasicAuth(obj.nexus_username, obj.nexus_password),
+        )
+    except Exception as e:
+        print(e)
+        print("Could not reach nexus at " + url)
+        exit(1)
+
+    print(resp.content.decode("utf-8"))
+
+
+@permissions.command("list", help="Show permissions")
+@click.pass_obj
+def list_permission(obj):
+    url = urljoin(obj.nexus_base_url, f"/permissions")
+    try:
+        resp = get(url, auth=auth.HTTPBasicAuth(obj.nexus_username, 
obj.nexus_password))
+    except Exception as e:
+        print(e)
+        print("Could not reach nexus at " + url)
+        exit(1)
+
+    print(resp.content.decode("utf-8"))
+
+@permissions.command("grant", help="Grant permission to a subject")
+@click.pass_obj
+@click.argument("subject-type")
+@click.argument("subject-id")
+@click.argument("resource-type")
+@click.argument("resource-id")
+@click.argument("permission-name")
+def grant_permission(obj, subject_type, subject_id, resource_type, 
resource_id, permission_name):
+    url = urljoin(obj.nexus_base_url, f"/permissions")
+    try:
+        permission = dict(
+            subjectType=subject_type,
+            subjectId=subject_id,
+            resourceType=resource_type,
+            resourceId=resource_id,
+            permissionName=permission_name,
+        )
+        body = dict(
+            permission=permission,
+            action="grant",
+        )
+        resp = post(url, json=body, 
auth=auth.HTTPBasicAuth(obj.nexus_username, obj.nexus_password))
+    except Exception as e:
+        print(e)
+        print("Could not reach nexus at " + url)
+        exit(1)
+
+    print(resp.content.decode("utf-8"))
+
+@permissions.command("revoke", help="Revoke permission from a subject")
+@click.pass_obj
+@click.argument("subject-type")
+@click.argument("subject-id")
+@click.argument("resource-type")
+@click.argument("resource-id")
+@click.argument("permission-name")
+def grant_permission(obj, subject_type, subject_id, resource_type, 
resource_id, permission_name):
+    url = urljoin(obj.nexus_base_url, f"/permissions")
+    try:
+        permission = dict(
+            subjectType=subject_type,
+            subjectId=subject_id,
+            resourceType=resource_type,
+            resourceId=resource_id,
+            permissionName=permission_name,
+        )
+        body = dict(
+            permission=permission,
+            action="revoke",
+        )
+        resp = post(url, json=body, 
auth=auth.HTTPBasicAuth(obj.nexus_username, obj.nexus_password))
+    except Exception as e:
+        print(e)
+        print("Could not reach nexus at " + url)
+        exit(1)
+
+    print(resp.content.decode("utf-8"))
+
+
 @cli.group()
 @click.pass_context
 def accounts(ctx):
@@ -86,6 +209,49 @@ class SandboxContext:
         return sandbox_base_url
 
 
+class NexusContext:
+    def __init__(self):
+        self._nexus_base_url = None
+        self._nexus_password = None
+        self._nexus_username = None
+
+    @property
+    def nexus_base_url(self):
+        if self._nexus_base_url:
+            return self._nexus_base_url
+        val = os.environ.get("LIBEUFIN_NEXUS_URL")
+        if not val:
+            raise click.UsageError(
+                "nexus URL must be given as an argument or in 
LIBEUFIN_NEXUS_URL"
+            )
+        self._nexus_base_url = val
+        return val
+
+    @property
+    def nexus_username(self):
+        if self._nexus_username:
+            return self._nexus_username
+        val = os.environ.get("LIBEUFIN_NEXUS_USERNAME")
+        if not val:
+            raise click.UsageError(
+                "nexus username must be given as an argument or in 
LIBEUFIN_NEXUS_USERNAME"
+            )
+        self._nexus_username = val
+        return val
+
+    @property
+    def nexus_password(self):
+        if self._nexus_password:
+            return self._nexus_password
+        val = os.environ.get("LIBEUFIN_NEXUS_PASSWORD")
+        if not val:
+            raise click.UsageError(
+                "nexus password must be given as an argument or in 
LIBEUFIN_NEXUS_PASSWORD"
+            )
+        self._nexus_password = val
+        return val
+
+
 @cli.group()
 @click.option("--sandbox-url", help="URL for the sandbox", required=False)
 @click.pass_context
@@ -250,7 +416,9 @@ def sync(obj, connection_name):
 )
 @click.argument("connection-name")
 @click.pass_obj
-def import_bank_account(obj, connection_name, offered_account_id, 
nexus_bank_account_id):
+def import_bank_account(
+    obj, connection_name, offered_account_id, nexus_bank_account_id
+):
     url = urljoin(
         obj.nexus_base_url,
         "/bank-connections/{}/import-account".format(connection_name),
@@ -322,6 +490,7 @@ def list_offered_bank_accounts(obj, connection_name):
 
     tell_user(resp, withsuccess=True)
 
+
 @accounts.command(help="Schedules a new task")
 @click.argument("account-name")
 @click.option("--task-name", help="Name of the task", required=True)
@@ -499,6 +668,7 @@ def submit_payment(obj, account_name, payment_uuid):
 
     tell_user(resp)
 
+
 @accounts.command(help="fetch transactions from the bank")
 @click.option(
     "--range-type",
@@ -530,7 +700,7 @@ def fetch_transactions(obj, account_name, range_type, 
level):
     "--compact/--no-compact",
     help="Tells only amount/subject for each payment",
     required=False,
-    default=False
+    default=False,
 )
 @click.argument("account-name")
 @click.pass_obj
@@ -547,15 +717,20 @@ def transactions(obj, compact, account_name):
     if compact and resp.status_code == 200:
         for payment in resp.json()["transactions"]:
             for entry in payment["batches"]:
-              for expected_singleton in entry["batchTransactions"]:
-                  print("{}, {}".format(
-                      
expected_singleton["details"]["unstructuredRemittanceInformation"],
-                      expected_singleton["amount"]
-                  ))
+                for expected_singleton in entry["batchTransactions"]:
+                    print(
+                        "{}, {}".format(
+                            expected_singleton["details"][
+                                "unstructuredRemittanceInformation"
+                            ],
+                            expected_singleton["amount"],
+                        )
+                    )
         return
 
     tell_user(resp, withsuccess=True)
 
+
 @facades.command(help="List active facades in the Nexus")
 @click.argument("connection-name")
 @click.pass_obj
@@ -590,7 +765,7 @@ def new_facade(obj, facade_name, connection_name, 
account_name, currency):
                     bankAccount=account_name,
                     bankConnection=connection_name,
                     reserveTransferLevel="UNUSED",
-                    intervalIncremental="UNUSED"
+                    intervalIncremental="UNUSED",
                 ),
             ),
         )
@@ -695,7 +870,9 @@ def sandbox_ebicsbankaccount(ctx):
     pass
 
 
-@sandbox_ebicsbankaccount.command("create", help="Create a bank account for a 
EBICS subscriber.")
+@sandbox_ebicsbankaccount.command(
+    "create", help="Create a bank account for a EBICS subscriber."
+)
 @click.option("--currency", help="currency", prompt=True)
 @click.option("--iban", help="IBAN", required=True)
 @click.option("--bic", help="BIC", required=True)
@@ -703,7 +880,9 @@ def sandbox_ebicsbankaccount(ctx):
 @click.option("--account-name", help="label of this bank account", 
required=True)
 @click.option("--ebics-user-id", help="user ID of the Ebics subscriber", 
required=True)
 @click.option("--ebics-host-id", help="host ID of the Ebics subscriber", 
required=True)
-@click.option("--ebics-partner-id", help="partner ID of the Ebics subscriber", 
required=True)
+@click.option(
+    "--ebics-partner-id", help="partner ID of the Ebics subscriber", 
required=True
+)
 @click.pass_obj
 def associate_bank_account(
     obj,
@@ -811,7 +990,7 @@ def bankaccount_generate_transactions(obj, account_label):
 @click.option(
     "--direction",
     help="direction respect to the bank account hosted at Sandbox: allows 
DBIT/CRDT values.",
-    prompt=True
+    prompt=True,
 )
 @click.pass_obj
 def book_payment(
@@ -839,7 +1018,7 @@ def book_payment(
         amount=amount,
         currency=currency,
         subject=subject,
-        direction=direction
+        direction=direction,
     )
     try:
         resp = post(url, json=body)
@@ -849,4 +1028,5 @@ def book_payment(
 
     tell_user(resp)
 
+
 cli(obj={})
diff --git a/cli/setup-template.sh b/cli/setup-template.sh
index 05b42a5..09e93b0 100755
--- a/cli/setup-template.sh
+++ b/cli/setup-template.sh
@@ -31,9 +31,9 @@ NEXUS_BANK_CONNECTION_NAME=b
 
 # Needed env
 
-export NEXUS_BASE_URL=$NEXUS_URL \
-       NEXUS_USERNAME=$NEXUS_USER \
-       NEXUS_PASSWORD=$NEXUS_PASSWORD \
+export LIBEUFIN_NEXUS_URL=$NEXUS_URL \
+       LIBEUFIN_NEXUS_USERNAME=$NEXUS_USER \
+       LIBEUFIN_NEXUS_PASSWORD=$NEXUS_PASSWORD \
        LIBEUFIN_SANDBOX_URL=$SANDBOX_URL
 
 echo Remove old database.
diff --git a/nexus/build.gradle b/nexus/build.gradle
index e048a85..e46e6cf 100644
--- a/nexus/build.gradle
+++ b/nexus/build.gradle
@@ -51,13 +51,13 @@ compileTestKotlin {
     }
 }
 
-def ktor_version = "1.3.2"
+def ktor_version = "1.5.0"
 def exposed_version = "0.25.1"
 
 dependencies {
     // Core language libraries
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
-    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
 
     // LibEuFin util library
     implementation project(":util")
@@ -72,7 +72,6 @@ dependencies {
     implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1"
     implementation 'org.apache.santuario:xmlsec:2.1.4'
 
-    //implementation "javax.activation:activation:1.1"
     // Compression
     implementation group: 'org.apache.commons', name: 'commons-compress', 
version: '1.20'
 
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
new file mode 100644
index 0000000..cc371a1
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
@@ -0,0 +1,116 @@
+package tech.libeufin.nexus
+
+import io.ktor.application.*
+import io.ktor.http.*
+import io.ktor.request.*
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.server.Permission
+import tech.libeufin.nexus.server.PermissionQuery
+import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.base64ToBytes
+import tech.libeufin.util.constructXml
+
+
+/**
+ * This helper function parses a Authorization:-header line, decode the 
credentials
+ * and returns a pair made of username and hashed (sha256) password.  The 
hashed value
+ * will then be compared with the one kept into the database.
+ */
+private fun extractUserAndPassword(authorizationHeader: String): Pair<String, 
String> {
+    logger.debug("Authenticating: $authorizationHeader")
+    val (username, password) = try {
+        val split = authorizationHeader.split(" ")
+        val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
+        plainUserAndPass.split(":")
+    } catch (e: java.lang.Exception) {
+        throw NexusError(
+            HttpStatusCode.BadRequest,
+            "invalid Authorization:-header received"
+        )
+    }
+    return Pair(username, password)
+}
+
+
+/**
+ * Test HTTP basic auth.  Throws error if password is wrong,
+ * and makes sure that the user exists in the system.
+ *
+ * @return user entity
+ */
+fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
+    return transaction {
+        val authorization = request.headers["Authorization"]
+        val headerLine = if (authorization == null) throw NexusError(
+            HttpStatusCode.BadRequest, "Authorization header not found"
+        ) else authorization
+        val (username, password) = extractUserAndPassword(headerLine)
+        val user = NexusUserEntity.find {
+            NexusUsersTable.id eq username
+        }.firstOrNull()
+        if (user == null) {
+            throw NexusError(HttpStatusCode.Unauthorized, "Unknown user 
'$username'")
+        }
+        if (!CryptoUtil.checkpw(password, user.passwordHash)) {
+            throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
+        }
+        user
+    }
+}
+
+fun requireSuperuser(request: ApplicationRequest): NexusUserEntity {
+    return transaction {
+        val user = authenticateRequest(request)
+        if (!user.superuser) {
+            throw NexusError(HttpStatusCode.Forbidden, "must be superuser")
+        }
+        user
+    }
+}
+
+fun findPermission(p: Permission): NexusPermissionEntity? {
+    return transaction {
+        NexusPermissionEntity.find {
+            ((NexusPermissionsTable.subjectType eq p.subjectType)
+                    and (NexusPermissionsTable.subjectId eq p.subjectId)
+                    and (NexusPermissionsTable.resourceType eq p.resourceType)
+                    and (NexusPermissionsTable.resourceId eq p.resourceId)
+                    and (NexusPermissionsTable.permissionName eq 
p.permissionName))
+
+        }.firstOrNull()
+    }
+}
+
+
+/**
+ * Require that the authenticated user has at least one of the listed 
permissions.
+ *
+ * Throws a NexusError if the authenticated user for the request doesn't have 
any of
+ * listed the permissions.
+ */
+fun ApplicationRequest.requirePermission(vararg perms: PermissionQuery) {
+    transaction {
+        val user = authenticateRequest(this@requirePermission)
+        if (user.superuser) {
+            return@transaction
+        }
+        var foundPermission = false
+        for (pr in perms) {
+            val p = Permission("user", user.id.value, pr.resourceType, 
pr.resourceId, pr.permissionName)
+            val existingPerm = findPermission(p)
+            if (existingPerm != null) {
+                foundPermission = true
+                break
+            }
+        }
+        if (!foundPermission) {
+            val possiblePerms =
+                perms.joinToString(" | ") { "${it.resourceId} 
${it.resourceType} ${it.permissionName}" }
+            throw NexusError(
+                HttpStatusCode.Forbidden,
+                "User ${user.id.value} has insufficient permissions (needs 
$possiblePerms."
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index c164aee..c51926e 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -38,7 +38,8 @@ import java.sql.Connection
  * whether a pain.001 document was sent or not to the bank is indicated
  * in the PAIN-table.
  */
-object TalerRequestedPayments : LongIdTable() {
+object TalerRequestedPaymentsTable : LongIdTable() {
+    val facade = reference("facade", FacadesTable)
     val preparedPayment = reference("payment", PaymentInitiationsTable)
     val requestUId = text("request_uid")
     val amount = text("amount")
@@ -48,21 +49,22 @@ object TalerRequestedPayments : LongIdTable() {
 }
 
 class TalerRequestedPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPayments)
-
-    var preparedPayment by PaymentInitiationEntity referencedOn 
TalerRequestedPayments.preparedPayment
-    var requestUId by TalerRequestedPayments.requestUId
-    var amount by TalerRequestedPayments.amount
-    var exchangeBaseUrl by TalerRequestedPayments.exchangeBaseUrl
-    var wtid by TalerRequestedPayments.wtid
-    var creditAccount by TalerRequestedPayments.creditAccount
+    companion object : 
LongEntityClass<TalerRequestedPaymentEntity>(TalerRequestedPaymentsTable)
+
+    var facade by FacadeEntity referencedOn TalerRequestedPaymentsTable.facade
+    var preparedPayment by PaymentInitiationEntity referencedOn 
TalerRequestedPaymentsTable.preparedPayment
+    var requestUId by TalerRequestedPaymentsTable.requestUId
+    var amount by TalerRequestedPaymentsTable.amount
+    var exchangeBaseUrl by TalerRequestedPaymentsTable.exchangeBaseUrl
+    var wtid by TalerRequestedPaymentsTable.wtid
+    var creditAccount by TalerRequestedPaymentsTable.creditAccount
 }
 
 /**
  * This is the table of the incoming payments.  Entries are merely "pointers" 
to the
- * entries from the raw payments table.  Fixme: name should end with "-table".
+ * entries from the raw payments table.
  */
-object TalerIncomingPayments : LongIdTable() {
+object TalerIncomingPaymentsTable : LongIdTable() {
     val payment = reference("payment", NexusBankTransactionsTable)
     val reservePublicKey = text("reservePublicKey")
     val timestampMs = long("timestampMs")
@@ -70,12 +72,12 @@ object TalerIncomingPayments : LongIdTable() {
 }
 
 class TalerIncomingPaymentEntity(id: EntityID<Long>) : LongEntity(id) {
-    companion object : 
LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPayments)
+    companion object : 
LongEntityClass<TalerIncomingPaymentEntity>(TalerIncomingPaymentsTable)
 
-    var payment by NexusBankTransactionEntity referencedOn 
TalerIncomingPayments.payment
-    var reservePublicKey by TalerIncomingPayments.reservePublicKey
-    var timestampMs by TalerIncomingPayments.timestampMs
-    var debtorPaytoUri by TalerIncomingPayments.debtorPaytoUri
+    var payment by NexusBankTransactionEntity referencedOn 
TalerIncomingPaymentsTable.payment
+    var reservePublicKey by TalerIncomingPaymentsTable.reservePublicKey
+    var timestampMs by TalerIncomingPaymentsTable.timestampMs
+    var debtorPaytoUri by TalerIncomingPaymentsTable.debtorPaytoUri
 }
 
 /**
@@ -93,6 +95,7 @@ object NexusBankMessagesTable : IntIdTable() {
 
 class NexusBankMessageEntity(id: EntityID<Int>) : IntEntity(id) {
     companion object : 
IntEntityClass<NexusBankMessageEntity>(NexusBankMessagesTable)
+
     var bankConnection by NexusBankConnectionEntity referencedOn 
NexusBankMessagesTable.bankConnection
     var messageId by NexusBankMessagesTable.messageId
     var code by NexusBankMessagesTable.code
@@ -218,6 +221,7 @@ object OfferedBankAccountsTable : Table() {
     val iban = text("iban")
     val bankCode = text("bankCode")
     val accountHolder = text("holderName")
+
     // column below gets defined only WHEN the user imports the bank account.
     val imported = reference("imported", NexusBankAccountsTable).nullable()
 
@@ -237,6 +241,7 @@ object NexusBankAccountsTable : IdTable<String>() {
     val lastStatementCreationTimestamp = 
long("lastStatementCreationTimestamp").nullable()
     val lastReportCreationTimestamp = 
long("lastReportCreationTimestamp").nullable()
     val lastNotificationCreationTimestamp = 
long("lastNotificationCreationTimestamp").nullable()
+
     // Highest bank message ID that this bank account is aware of.
     val highestSeenBankMessageId = integer("highestSeenBankMessageId")
     val pain001Counter = long("pain001counter").default(1)
@@ -244,6 +249,7 @@ object NexusBankAccountsTable : IdTable<String>() {
 
 class NexusBankAccountEntity(id: EntityID<String>) : Entity<String>(id) {
     companion object : EntityClass<String, 
NexusBankAccountEntity>(NexusBankAccountsTable)
+
     var accountHolder by NexusBankAccountsTable.accountHolder
     var iban by NexusBankAccountsTable.iban
     var bankCode by NexusBankAccountsTable.bankCode
@@ -297,6 +303,7 @@ object NexusUsersTable : IdTable<String>() {
 
 class NexusUserEntity(id: EntityID<String>) : Entity<String>(id) {
     companion object : EntityClass<String, NexusUserEntity>(NexusUsersTable)
+
     var passwordHash by NexusUsersTable.passwordHash
     var superuser by NexusUsersTable.superuser
 }
@@ -309,6 +316,7 @@ object NexusBankConnectionsTable : IdTable<String>() {
 
 class NexusBankConnectionEntity(id: EntityID<String>) : Entity<String>(id) {
     companion object : EntityClass<String, 
NexusBankConnectionEntity>(NexusBankConnectionsTable)
+
     var type by NexusBankConnectionsTable.type
     var owner by NexusUserEntity referencedOn NexusBankConnectionsTable.owner
 }
@@ -376,6 +384,34 @@ class NexusScheduledTaskEntity(id: EntityID<Int>) : 
IntEntity(id) {
     var prevScheduledExecutionSec by 
NexusScheduledTasksTable.prevScheduledExecutionSec
 }
 
+/**
+ * Generic permissions table that determines access of a subject
+ * identified by (subjectType, subjectName) to a resource (resourceType, 
resourceId).
+ *
+ * Subjects are typically of type "user", but this may change in the future.
+ */
+object NexusPermissionsTable : IntIdTable() {
+    val resourceType = text("resourceType")
+    val resourceId = text("resourceId")
+    val subjectType = text("subjectType")
+    val subjectId = text("subjectName")
+    val permissionName = text("permissionName")
+
+    init {
+        uniqueIndex(resourceType, resourceId, subjectType, subjectId, 
permissionName)
+    }
+}
+
+class NexusPermissionEntity(id: EntityID<Int>) : IntEntity(id) {
+    companion object : 
IntEntityClass<NexusPermissionEntity>(NexusPermissionsTable)
+
+    var resourceType by NexusPermissionsTable.resourceType
+    var resourceId by NexusPermissionsTable.resourceId
+    var subjectType by NexusPermissionsTable.subjectType
+    var subjectId by NexusPermissionsTable.subjectId
+    var permissionName by NexusPermissionsTable.permissionName
+}
+
 fun dbDropTables(dbConnectionString: String) {
     Database.connect(dbConnectionString)
     transaction {
@@ -385,20 +421,21 @@ fun dbDropTables(dbConnectionString: String) {
             NexusEbicsSubscribersTable,
             NexusBankAccountsTable,
             NexusBankTransactionsTable,
-            TalerIncomingPayments,
-            TalerRequestedPayments,
+            TalerIncomingPaymentsTable,
+            TalerRequestedPaymentsTable,
             NexusBankConnectionsTable,
             NexusBankMessagesTable,
             FacadesTable,
             TalerFacadeStateTable,
             NexusScheduledTasksTable,
-            OfferedBankAccountsTable
+            OfferedBankAccountsTable,
+            NexusPermissionsTable,
         )
     }
 }
 
 fun dbCreateTables(dbConnectionString: String) {
-    Database.connect("$dbConnectionString")
+    Database.connect(dbConnectionString)
     TransactionManager.manager.defaultIsolationLevel = 
Connection.TRANSACTION_SERIALIZABLE
     transaction {
         SchemaUtils.create(
@@ -407,15 +444,16 @@ fun dbCreateTables(dbConnectionString: String) {
             NexusEbicsSubscribersTable,
             NexusBankAccountsTable,
             NexusBankTransactionsTable,
-            TalerIncomingPayments,
-            TalerRequestedPayments,
+            TalerIncomingPaymentsTable,
+            TalerRequestedPaymentsTable,
             NexusBankConnectionsTable,
             NexusBankMessagesTable,
             FacadesTable,
             TalerFacadeStateTable,
             NexusScheduledTasksTable,
             OfferedBankAccountsTable,
-            NexusScheduledTasksTable
+            NexusScheduledTasksTable,
+            NexusPermissionsTable,
         )
     }
 }
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index 82c3efe..2d17b8e 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -46,7 +46,6 @@ const val DEFAULT_DB_CONNECTION = 
"jdbc:sqlite:/tmp/libeufin-nexus.sqlite3"
 
 class NexusCommand : CliktCommand() {
     init {
-        // FIXME: Obtain actual version number!
         versionOption(getVersion())
     }
     override fun run() = Unit
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
index ddf6797..9e6f6fd 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -197,32 +197,20 @@ private fun getTalerFacadeState(fcid: String): 
TalerFacadeStateEntity {
         HttpStatusCode.NotFound,
         "Could not find facade '${fcid}'"
     )
-    val facadeState = TalerFacadeStateEntity.find {
+    return TalerFacadeStateEntity.find {
         TalerFacadeStateTable.facade eq facade.id.value
     }.firstOrNull() ?: throw NexusError(
         HttpStatusCode.NotFound,
-        "Could not find any state for facade: ${fcid}"
+        "Could not find any state for facade: $fcid"
     )
-    return facadeState
 }
 
 private fun getTalerFacadeBankAccount(fcid: String): NexusBankAccountEntity {
-    val facade = FacadeEntity.find { FacadesTable.id eq fcid }.firstOrNull() 
?: throw NexusError(
-        HttpStatusCode.NotFound,
-        "Could not find facade '${fcid}'"
-    )
-    val facadeState = TalerFacadeStateEntity.find {
-        TalerFacadeStateTable.facade eq facade.id.value
-    }.firstOrNull() ?: throw NexusError(
-        HttpStatusCode.NotFound,
-        "Could not find any state for facade: ${fcid}"
-    )
-    val bankAccount = NexusBankAccountEntity.findById(facadeState.bankAccount) 
?: throw NexusError(
+    val facadeState = getTalerFacadeState(fcid)
+    return NexusBankAccountEntity.findById(facadeState.bankAccount) ?: throw 
NexusError(
         HttpStatusCode.NotFound,
         "Could not find any bank account named ${facadeState.bankAccount}"
     )
-
-    return bankAccount
 }
 
 /**
@@ -232,13 +220,19 @@ private suspend fun talerTransfer(call: ApplicationCall) {
     val transferRequest = call.receive<TalerTransferRequest>()
     val amountObj = parseAmount(transferRequest.amount)
     val creditorObj = parsePayto(transferRequest.credit_account)
+    val facadeId = expectNonNull(call.parameters["fcid"])
     val opaqueRowId = transaction {
         // FIXME: re-enable authentication 
(https://bugs.gnunet.org/view.php?id=6703)
         // val exchangeUser = authenticateRequest(call.request)
+        call.request.requirePermission(PermissionQuery("facade", facadeId, 
"facade.talerWireGateway.transfer"))
+        val facade = FacadeEntity.find { FacadesTable.id eq facadeId 
}.firstOrNull() ?: throw NexusError(
+            HttpStatusCode.NotFound,
+            "Could not find facade '${facadeId}'"
+        )
         val creditorData = parsePayto(transferRequest.credit_account)
         /** Checking the UID has the desired characteristics */
         TalerRequestedPaymentEntity.find {
-            TalerRequestedPayments.requestUId eq transferRequest.request_uid
+            TalerRequestedPaymentsTable.requestUId eq 
transferRequest.request_uid
         }.forEach {
             if (
                 (it.amount != transferRequest.amount) or
@@ -251,7 +245,7 @@ private suspend fun talerTransfer(call: ApplicationCall) {
                 )
             }
         }
-        val exchangeBankAccount = 
getTalerFacadeBankAccount(expectNonNull(call.parameters["fcid"]))
+        val exchangeBankAccount = getTalerFacadeBankAccount(facadeId)
         val pain001 = addPaymentInitiation(
             Pain001Data(
                 creditorIban = creditorData.iban,
@@ -265,6 +259,7 @@ private suspend fun talerTransfer(call: ApplicationCall) {
         )
         logger.debug("Taler requests payment: ${transferRequest.wtid}")
         val row = TalerRequestedPaymentEntity.new {
+            this.facade = facade
             preparedPayment = pain001 // not really used/needed, just here to 
silence warnings
             exchangeBaseUrl = transferRequest.exchange_base_url
             requestUId = transferRequest.request_uid
@@ -299,11 +294,11 @@ fun roundTimestamp(t: GnunetTimestamp): GnunetTimestamp {
  * Serve a /taler/admin/add-incoming
  */
 private suspend fun talerAddIncoming(call: ApplicationCall, httpClient: 
HttpClient): Unit {
+    val facadeID = expectNonNull(call.parameters["fcid"])
+    call.request.requirePermission(PermissionQuery("facade", facadeID, 
"facade.talerWireGateway.addIncoming"))
     val addIncomingData = call.receive<TalerAdminAddIncoming>()
     val debtor = parsePayto(addIncomingData.debit_account)
     val res = transaction {
-        val user = authenticateRequest(call.request)
-        val facadeID = expectNonNull(call.parameters["fcid"])
         val facadeState = getTalerFacadeState(facadeID)
         val facadeBankAccount = getTalerFacadeBankAccount(facadeID)
         return@transaction object {
@@ -313,6 +308,7 @@ private suspend fun talerAddIncoming(call: ApplicationCall, 
httpClient: HttpClie
             val facadeHolderName = facadeBankAccount.accountHolder
         }
     }
+
     /** forward the payment information to the sandbox.  */
     val response = httpClient.post<HttpResponse>(
         urlString = "http://localhost:5000/admin/payments";,
@@ -366,19 +362,19 @@ private fun ingestIncoming(payment: 
NexusBankTransactionEntity, txDtls: Transact
     val debtorAcct = txDtls.debtorAccount
     if (debtorAcct == null) {
         // FIXME: Report payment, we can't even send it back
-        logger.warn("empty debitor account")
+        logger.warn("empty debtor account")
         return
     }
     val debtorIban = debtorAcct.iban
     if (debtorIban == null) {
         // FIXME: Report payment, we can't even send it back
-        logger.warn("non-iban debitor account")
+        logger.warn("non-iban debtor account")
         return
     }
     val debtorAgent = txDtls.debtorAgent
     if (debtorAgent == null) {
         // FIXME: Report payment, we can't even send it back
-        logger.warn("missing debitor agent")
+        logger.warn("missing debtor agent")
         return
     }
     val reservePub = extractReservePubFromSubject(subject)
@@ -440,6 +436,7 @@ fun ingestTalerTransactions() {
             }
             when (tx.creditDebitIndicator) {
                 CreditDebitIndicator.CRDT -> ingestIncoming(it, txDtls = 
details)
+                else -> Unit
             }
             lastId = it.id.value
         }
@@ -460,6 +457,8 @@ fun ingestTalerTransactions() {
  * Handle a /taler/history/outgoing request.
  */
 private suspend fun historyOutgoing(call: ApplicationCall) {
+    val facadeId = expectNonNull(call.parameters["fcid"])
+    call.request.requirePermission(PermissionQuery("facade", facadeId, 
"facade.talerWireGateway.history"))
     val param = call.expectUrlParameter("delta")
     val delta: Int = try {
         param.toInt()
@@ -467,14 +466,12 @@ private suspend fun historyOutgoing(call: 
ApplicationCall) {
         throw EbicsProtocolError(HttpStatusCode.BadRequest, "'${param}' is not 
Int")
     }
     val start: Long = 
handleStartArgument(call.request.queryParameters["start"], delta)
-    val startCmpOp = getComparisonOperator(delta, start, 
TalerRequestedPayments)
+    val startCmpOp = getComparisonOperator(delta, start, 
TalerRequestedPaymentsTable)
     /* retrieve database elements */
     val history = TalerOutgoingHistory()
     transaction {
-        val user = authenticateRequest(call.request)
-
         /** Retrieve all the outgoing payments from the _clean Taler outgoing 
table_ */
-        val subscriberBankAccount = 
getTalerFacadeBankAccount(expectNonNull(call.parameters["fcid"]))
+        val subscriberBankAccount = getTalerFacadeBankAccount(facadeId)
         val reqPayments = mutableListOf<TalerRequestedPaymentEntity>()
         val reqPaymentsWithUnconfirmed = TalerRequestedPaymentEntity.find {
             startCmpOp
@@ -509,9 +506,11 @@ private suspend fun historyOutgoing(call: ApplicationCall) 
{
 }
 
 /**
- * Handle a /taler/history/incoming request.
+ * Handle a /taler-wire-gateway/history/incoming request.
  */
 private suspend fun historyIncoming(call: ApplicationCall): Unit {
+    val facadeId = expectNonNull(call.parameters["fcid"])
+    call.request.requirePermission(PermissionQuery("facade", facadeId, 
"facade.talerWireGateway.history"))
     val param = call.expectUrlParameter("delta")
     val delta: Int = try {
         param.toInt()
@@ -520,7 +519,7 @@ private suspend fun historyIncoming(call: ApplicationCall): 
Unit {
     }
     val start: Long = 
handleStartArgument(call.request.queryParameters["start"], delta)
     val history = TalerIncomingHistory()
-    val startCmpOp = getComparisonOperator(delta, start, TalerIncomingPayments)
+    val startCmpOp = getComparisonOperator(delta, start, 
TalerIncomingPaymentsTable)
     transaction {
         val orderedPayments = TalerIncomingPaymentEntity.find {
             startCmpOp
@@ -562,12 +561,14 @@ private fun getCurrency(facadeName: String): String {
 }
 
 fun talerFacadeRoutes(route: Route, httpClient: HttpClient) {
+
     route.get("/config") {
-        val facadeName = ensureNonNull(call.parameters["fcid"])
+        val facadeId = ensureNonNull(call.parameters["fcid"])
+        call.request.requirePermission(PermissionQuery("facade", facadeId, 
"facade.talerWireGateway.addIncoming"))
         call.respond(object {
             val version = "0.0.0"
-            val name = facadeName
-            val currency = getCurrency(facadeName)
+            val name = "taler-wire-gateway"
+            val currency = getCurrency(facadeId)
         })
         return@get
     }
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index fa827f8..23dccf3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -32,13 +32,9 @@ import 
com.fasterxml.jackson.databind.annotation.JsonDeserialize
 import com.fasterxml.jackson.databind.annotation.JsonSerialize
 import com.fasterxml.jackson.databind.deser.std.StdDeserializer
 import com.fasterxml.jackson.databind.ser.std.StdSerializer
-import tech.libeufin.nexus.NexusScheduledTasksTable
-import tech.libeufin.nexus.NexusScheduledTasksTable.nullable
 import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
-import tech.libeufin.nexus.iso20022.CreditDebitIndicator
 import tech.libeufin.nexus.iso20022.EntryStatus
 import tech.libeufin.util.*
-import java.lang.UnsupportedOperationException
 import java.math.BigDecimal
 import java.time.Instant
 import java.time.ZoneId
@@ -88,13 +84,13 @@ object EbicsDateFormat {
         .parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
         .parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
         .parseDefaulting(ChronoField.OFFSET_SECONDS, 
ZoneId.systemDefault().rules.getOffset(Instant.now()).totalSeconds.toLong())
-        .toFormatter()
+        .toFormatter()!!
 }
 
 @JsonTypeName("standard-date-range")
 class EbicsStandardOrderParamsDateJson(
-    val start: String,
-    val end: String
+    private val start: String,
+    private val end: String
 ) : EbicsOrderParamsJson() {
     override fun toOrderParams(): EbicsOrderParams {
         val dateRange: EbicsDateRange? =
@@ -149,6 +145,29 @@ data class EbicsKeysBackupJson(
     val sigBlob: String
 )
 
+enum class PermissionChangeAction(@get:JsonValue val jsonName: String) {
+    GRANT("grant"), REVOKE("revoke")
+}
+
+data class Permission(
+    val subjectType: String,
+    val subjectId: String,
+    val resourceType: String,
+    val resourceId: String,
+    val permissionName: String
+)
+
+data class PermissionQuery(
+    val resourceType: String,
+    val resourceId: String,
+    val permissionName: String,
+)
+
+data class ChangePermissionsRequest(
+    val action: PermissionChangeAction,
+    val permission: Permission
+)
+
 enum class FetchLevel(@get:JsonValue val jsonName: String) {
     REPORT("report"), STATEMENT("statement"), ALL("all");
 }
@@ -270,7 +289,7 @@ data class UserResponse(
 )
 
 /** Request type of "POST /users" */
-data class User(
+data class CreateUserRequest(
     val username: String,
     val password: String
 )
@@ -408,20 +427,6 @@ data class CurrencyAmount(
     val value: BigDecimal // allows calculations
 )
 
-/**
- * Account entry item as returned by the /bank-accounts/{acctId}/transactions 
API.
- */
-data class AccountEntryItemJson(
-    val nexusEntryId: String,
-    val nexusStatusSequenceId: Int,
-
-    val entryId: String?,
-    val accountServicerRef: String?,
-    val creditDebitIndicator: CreditDebitIndicator,
-    val entryAmount: CurrencyAmount,
-    val status: EntryStatus
-)
-
 data class InitiatedPayments(
     val initiatedPayments: MutableList<PaymentStatus> = mutableListOf()
 )
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index 3c8d06b..8c65427 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -46,9 +46,13 @@ import io.ktor.response.respondText
 import io.ktor.routing.*
 import io.ktor.server.engine.embeddedServer
 import io.ktor.server.netty.Netty
-import io.ktor.utils.io.ByteReadChannel
+import io.ktor.util.*
+import io.ktor.util.pipeline.*
+import io.ktor.utils.io.*
 import io.ktor.utils.io.jvm.javaio.toByteReadChannel
 import io.ktor.utils.io.jvm.javaio.toInputStream
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.select
 import org.jetbrains.exposed.sql.transactions.transaction
@@ -66,10 +70,11 @@ import tech.libeufin.util.*
 import tech.libeufin.nexus.logger
 import java.lang.IllegalArgumentException
 import java.net.URLEncoder
-import java.nio.file.Paths
 import java.util.zip.InflaterInputStream
 
-// Return facade state depending on the type.
+/**
+ * Return facade state depending on the type.
+ */
 fun getFacadeState(type: String, facade: FacadeEntity): JsonNode {
     return transaction {
         when (type) {
@@ -83,7 +88,7 @@ fun getFacadeState(type: String, facade: FacadeEntity): 
JsonNode {
                 node.put("bankAccount", state.bankAccount)
                 node
             }
-            else -> throw NexusError(HttpStatusCode.NotFound, "Facade type 
${type} not supported")
+            else -> throw NexusError(HttpStatusCode.NotFound, "Facade type 
$type not supported")
         }
     }
 }
@@ -91,21 +96,14 @@ fun getFacadeState(type: String, facade: FacadeEntity): 
JsonNode {
 
 fun ensureNonNull(param: String?): String {
     return param ?: throw NexusError(
-        HttpStatusCode.BadRequest, "Bad ID given: ${param}"
+        HttpStatusCode.BadRequest, "Bad ID given: $param"
     )
 }
 
 fun ensureLong(param: String?): Long {
     val asString = ensureNonNull(param)
     return asString.toLongOrNull() ?: throw NexusError(
-        HttpStatusCode.BadRequest, "Parameter is not Long: ${param}"
-    )
-}
-
-fun ensureInt(param: String?): Int {
-    val asString = ensureNonNull(param)
-    return asString.toIntOrNull() ?: throw NexusError(
-        HttpStatusCode.BadRequest, "Parameter is not Int: ${param}"
+        HttpStatusCode.BadRequest, "Parameter is not Long: $param"
     )
 }
 
@@ -116,52 +114,6 @@ fun <T> expectNonNull(param: T?): T {
     )
 }
 
-/**
- * This helper function parses a Authorization:-header line, decode the 
credentials
- * and returns a pair made of username and hashed (sha256) password.  The 
hashed value
- * will then be compared with the one kept into the database.
- */
-fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
-    logger.debug("Authenticating: $authorizationHeader")
-    val (username, password) = try {
-        val split = authorizationHeader.split(" ")
-        val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
-        plainUserAndPass.split(":")
-    } catch (e: java.lang.Exception) {
-        throw NexusError(
-            HttpStatusCode.BadRequest,
-            "invalid Authorization:-header received"
-        )
-    }
-    return Pair(username, password)
-}
-
-
-/**
- * Test HTTP basic auth.  Throws error if password is wrong,
- * and makes sure that the user exists in the system.
- *
- * @param authorization the Authorization:-header line.
- * @return user id
- */
-fun authenticateRequest(request: ApplicationRequest): NexusUserEntity {
-    val authorization = request.headers["Authorization"]
-    val headerLine = if (authorization == null) throw NexusError(
-        HttpStatusCode.BadRequest, "Authorization header not found"
-    ) else authorization
-    val (username, password) = extractUserAndPassword(headerLine)
-    val user = NexusUserEntity.find {
-        NexusUsersTable.id eq username
-    }.firstOrNull()
-    if (user == null) {
-        throw NexusError(HttpStatusCode.Unauthorized, "Unknown user 
'$username'")
-    }
-    if (!CryptoUtil.checkpw(password, user.passwordHash)) {
-        throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
-    }
-    return user
-}
-
 
 fun ApplicationRequest.hasBody(): Boolean {
     if (this.isChunked()) {
@@ -169,11 +121,11 @@ fun ApplicationRequest.hasBody(): Boolean {
     }
     val contentLengthHeaderStr = this.headers["content-length"]
     if (contentLengthHeaderStr != null) {
-        try {
+        return try {
             val cl = contentLengthHeaderStr.toInt()
-            return cl != 0
+            cl != 0
         } catch (e: NumberFormatException) {
-            return false
+            false
         }
     }
     return false
@@ -287,6 +239,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
                 )
             }
         }
+        install(RequestBodyDecompression)
         intercept(ApplicationCallPipeline.Fallback) {
             if (this.call.response.status() == null) {
                 call.respondText("Not found (no route matched).\n", 
ContentType.Text.Plain, HttpStatusCode.NotFound)
@@ -294,34 +247,12 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
         }
 
-        // Allow request body compression.  Needed by Taler.
-        receivePipeline.intercept(ApplicationReceivePipeline.Before) {
-            if (this.context.request.headers["Content-Encoding"] == "deflate") 
{
-                logger.debug("About to inflate received data")
-                val deflated = this.subject.value as ByteReadChannel
-                val inflated = InflaterInputStream(deflated.toInputStream())
-                proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, 
inflated.toByteReadChannel()))
-                return@intercept
-            }
-            proceed()
-            return@intercept
-        }
         startOperationScheduler(client)
         routing {
-            get("/service-config") {
-                call.respond(
-                    object {
-                        val dbConn = 
"sqlite://${Paths.get(dbName).toAbsolutePath()}"
-                    }
-                )
-                return@get
-            }
-
             get("/config") {
                 call.respond(
                     object {
-                        val version = "0.0.0"
-                        val currency = "EUR"
+                        val version = getVersion()
                     }
                 )
                 return@get
@@ -339,7 +270,58 @@ fun serverMain(dbName: String, host: String, port: Int) {
                 return@get
             }
 
+            get("/permissions") {
+                val resp = object {
+                    val permissions = mutableListOf<Permission>()
+                }
+                transaction {
+                    requireSuperuser(call.request)
+                    NexusPermissionEntity.all().map {
+                        resp.permissions.add(
+                            Permission(
+                                subjectType = it.subjectType,
+                                subjectId = it.subjectId,
+                                resourceType = it.resourceType,
+                                resourceId = it.resourceId,
+                                permissionName = it.permissionName,
+                            )
+                        )
+                    }
+                }
+                call.respond(resp)
+            }
+
+            post("/permissions") {
+                val req = call.receive<ChangePermissionsRequest>()
+                transaction {
+                    requireSuperuser(call.request)
+                    val existingPerm = findPermission(req.permission)
+                    when (req.action) {
+                        PermissionChangeAction.GRANT -> {
+                            if (existingPerm == null) {
+                                NexusPermissionEntity.new() {
+                                    subjectType = req.permission.subjectType
+                                    subjectId = req.permission.subjectId
+                                    resourceType = req.permission.resourceType
+                                    resourceId = req.permission.resourceId
+                                    permissionName = 
req.permission.permissionName
+
+                                }
+                            }
+                        }
+                        PermissionChangeAction.REVOKE -> {
+                            existingPerm?.delete()
+                        }
+                    }
+                    null
+                }
+                call.respond(object {})
+            }
+
             get("/users") {
+                transaction {
+                    requireSuperuser(call.request)
+                }
                 val users = transaction {
                     transaction {
                         NexusUserEntity.all().map {
@@ -354,19 +336,16 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
             // Add a new ordinary user in the system (requires superuser 
privileges)
             post("/users") {
-                val body = call.receiveJson<User>()
+                val body = call.receiveJson<CreateUserRequest>()
                 transaction {
-                    val currentUser = authenticateRequest(call.request)
-                    if (!currentUser.superuser) {
-                        throw NexusError(HttpStatusCode.Forbidden, "only 
superuser can do that")
-                    }
+                    requireSuperuser(call.request)
                     NexusUserEntity.new(body.username) {
                         passwordHash = CryptoUtil.hashpw(body.password)
                         superuser = false
                     }
                 }
                 call.respondText(
-                    "New NEXUS user registered. ID: ${body.username}",
+                    "New user '${body.username}' registered",
                     ContentType.Text.Plain,
                     HttpStatusCode.OK
                 )
@@ -374,6 +353,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/bank-connection-protocols") {
+                requireSuperuser(call.request)
                 call.respond(
                     HttpStatusCode.OK,
                     BankProtocolsResponse(listOf("ebics", "loopback"))
@@ -406,21 +386,23 @@ fun serverMain(dbName: String, host: String, port: Int) {
                 return@get
             }
             post("/bank-accounts/{accountId}/test-camt-ingestion/{type}") {
+                requireSuperuser(call.request)
                 processCamtMessage(
                     ensureNonNull(call.parameters["accountId"]),
                     XMLUtil.parseStringIntoDom(call.receiveText()),
                     ensureNonNull(call.parameters["type"])
                 )
-                call.respond({ })
+                call.respond(object {})
                 return@post
             }
             get("/bank-accounts/{accountid}/schedule") {
+                requireSuperuser(call.request)
                 val resp = jacksonObjectMapper().createObjectNode()
                 val ops = jacksonObjectMapper().createObjectNode()
                 val accountId = ensureNonNull(call.parameters["accountid"])
                 resp.set<JsonNode>("schedule", ops)
                 transaction {
-                    val bankAccount = 
NexusBankAccountEntity.findById(accountId)
+                    NexusBankAccountEntity.findById(accountId)
                         ?: throw NexusError(HttpStatusCode.NotFound, "unknown 
bank account")
                     NexusScheduledTaskEntity.find {
                         (NexusScheduledTasksTable.resourceType eq 
"bank-account") and
@@ -440,6 +422,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             post("/bank-accounts/{accountid}/schedule") {
+                requireSuperuser(call.request)
                 val schedSpec = call.receive<CreateAccountTaskRequest>()
                 val accountId = ensureNonNull(call.parameters["accountid"])
                 transaction {
@@ -486,6 +469,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/bank-accounts/{accountId}/schedule/{taskId}") {
+                requireSuperuser(call.request)
                 val task = transaction {
                     NexusScheduledTaskEntity.find {
                         NexusScheduledTasksTable.taskName eq 
ensureNonNull(call.parameters["taskId"])
@@ -511,6 +495,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             delete("/bank-accounts/{accountId}/schedule/{taskId}") {
+                requireSuperuser(call.request)
                 logger.info("schedule delete requested")
                 val accountId = ensureNonNull(call.parameters["accountId"])
                 val taskId = ensureNonNull(call.parameters["taskId"])
@@ -525,14 +510,13 @@ fun serverMain(dbName: String, host: String, port: Int) {
                                 (NexusScheduledTasksTable.resourceId eq 
accountId)
 
                     }.firstOrNull()
-                    if (oldSchedTask != null) {
-                        oldSchedTask.delete()
-                    }
+                    oldSchedTask?.delete()
                 }
                 call.respond(object {})
             }
 
             get("/bank-accounts/{accountid}") {
+                requireSuperuser(call.request)
                 val accountId = ensureNonNull(call.parameters["accountid"])
                 val res = transaction {
                     val user = authenticateRequest(call.request)
@@ -551,17 +535,19 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
             // Submit one particular payment to the bank.
             
post("/bank-accounts/{accountid}/payment-initiations/{uuid}/submit") {
+                requireSuperuser(call.request)
                 val uuid = ensureLong(call.parameters["uuid"])
                 val accountId = ensureNonNull(call.parameters["accountid"])
                 val res = transaction {
                     authenticateRequest(call.request)
                 }
                 submitPaymentInitiation(client, uuid)
-                call.respondText("Payment ${uuid} submitted")
+                call.respondText("Payment $uuid submitted")
                 return@post
             }
 
             post("/bank-accounts/{accountid}/submit-all-payment-initiations") {
+                requireSuperuser(call.request)
                 val accountId = ensureNonNull(call.parameters["accountid"])
                 val res = transaction {
                     authenticateRequest(call.request)
@@ -572,6 +558,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/bank-accounts/{accountid}/payment-initiations") {
+                requireSuperuser(call.request)
                 val ret = InitiatedPayments()
                 transaction {
                     val bankAccount = requireBankAccount(call, "accountid")
@@ -603,6 +590,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
             // Shows information about one particular payment initiation.
             get("/bank-accounts/{accountid}/payment-initiations/{uuid}") {
+                requireSuperuser(call.request)
                 val res = transaction {
                     val user = authenticateRequest(call.request)
                     val paymentInitiation = 
getPaymentInitiation(ensureLong(call.parameters["uuid"]))
@@ -633,6 +621,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
             // Adds a new payment initiation.
             post("/bank-accounts/{accountid}/payment-initiations") {
+                requireSuperuser(call.request)
                 val body = call.receive<CreatePaymentInitiationRequest>()
                 val accountId = ensureNonNull(call.parameters["accountid"])
                 val res = transaction {
@@ -666,6 +655,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
             // Downloads new transactions from the bank.
             post("/bank-accounts/{accountid}/fetch-transactions") {
+                requireSuperuser(call.request)
                 val accountid = call.parameters["accountid"]
                 if (accountid == null) {
                     throw NexusError(
@@ -691,6 +681,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
             // Asks list of transactions ALREADY downloaded from the bank.
             get("/bank-accounts/{accountid}/transactions") {
+                requireSuperuser(call.request)
                 val bankAccountId = expectNonNull(call.parameters["accountid"])
                 val start = call.request.queryParameters["start"]
                 val end = call.request.queryParameters["end"]
@@ -714,6 +705,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
             // Adds a new bank transport.
             post("/bank-connections") {
+                requireSuperuser(call.request)
                 // user exists and is authenticated.
                 val body = call.receive<CreateBankConnectionRequestJson>()
                 transaction {
@@ -759,6 +751,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             post("/bank-connections/delete-connection") {
+                requireSuperuser(call.request)
                 val body = call.receive<BankConnectionDeletion>()
                 transaction {
                     val conn = 
NexusBankConnectionEntity.findById(body.bankConnectionId) ?: throw NexusError(
@@ -771,6 +764,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/bank-connections") {
+                requireSuperuser(call.request)
                 val connList = BankConnectionsList()
                 transaction {
                     NexusBankConnectionEntity.all().forEach {
@@ -785,10 +779,11 @@ fun serverMain(dbName: String, host: String, port: Int) {
                 call.respond(connList)
             }
 
-            get("/bank-connections/{connid}") {
+            get("/bank-connections/{connectionId}") {
+                requireSuperuser(call.request)
                 val resp = transaction {
                     val user = authenticateRequest(call.request)
-                    val conn = requireBankConnection(call, "connid")
+                    val conn = requireBankConnection(call, "connectionId")
                     when (conn.type) {
                         "ebics" -> {
                             getEbicsConnectionDetails(conn)
@@ -805,6 +800,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             post("/bank-connections/{connid}/export-backup") {
+                requireSuperuser(call.request)
                 transaction { authenticateRequest(call.request) }
                 val body = call.receive<BackupRequestJson>()
                 val response = run {
@@ -829,6 +825,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             post("/bank-connections/{connid}/connect") {
+                requireSuperuser(call.request)
                 val conn = transaction {
                     authenticateRequest(call.request)
                     requireBankConnection(call, "connid")
@@ -842,6 +839,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/bank-connections/{connid}/keyletter") {
+                requireSuperuser(call.request)
                 val conn = transaction {
                     authenticateRequest(call.request)
                     requireBankConnection(call, "connid")
@@ -856,6 +854,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/bank-connections/{connid}/messages") {
+                requireSuperuser(call.request)
                 val ret = transaction {
                     val list = BankMessageList()
                     val conn = requireBankConnection(call, "connid")
@@ -874,6 +873,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/bank-connections/{connid}/messages/{msgid}") {
+                requireSuperuser(call.request)
                 val ret = transaction {
                     val msgid = call.parameters["msgid"]
                     if (msgid == null || msgid == "") {
@@ -889,6 +889,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/facades/{fcid}") {
+                requireSuperuser(call.request)
                 val fcid = ensureNonNull(call.parameters["fcid"])
                 val ret = transaction {
                     val f = FacadeEntity.findById(fcid) ?: throw NexusError(
@@ -906,6 +907,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             get("/facades") {
+                requireSuperuser(call.request)
                 val ret = object {
                     val facades = mutableListOf<FacadeShowInfo>()
                 }
@@ -929,6 +931,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             post("/facades") {
+                requireSuperuser(call.request)
                 val body = call.receive<FacadeInfo>()
                 if (body.type != "taler-wire-gateway") throw NexusError(
                     HttpStatusCode.NotImplemented,
@@ -955,11 +958,13 @@ fun serverMain(dbName: String, host: String, port: Int) {
             }
 
             route("/bank-connections/{connid}") {
+
                 // only ebics specific tasks under this part.
                 route("/ebics") {
                     ebicsBankConnectionRoutes(client)
                 }
                 post("/fetch-accounts") {
+                    requireSuperuser(call.request)
                     val conn = transaction {
                         authenticateRequest(call.request)
                         requireBankConnection(call, "connid")
@@ -978,6 +983,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
 
                 // show all the offered accounts (both imported and non)
                 get("/accounts") {
+                    requireSuperuser(call.request)
                     val ret = OfferedBankAccounts()
                     transaction {
                         val conn = requireBankConnection(call, "connid")
@@ -997,8 +1003,10 @@ fun serverMain(dbName: String, host: String, port: Int) {
                     }
                     call.respond(ret)
                 }
+
                 // import one account into libeufin.
                 post("/import-account") {
+                    requireSuperuser(call.request)
                     val body = call.receive<ImportBankAccount>()
                     importBankAccount(call, body.offeredAccountId, 
body.nexusBankAccountId)
                     call.respond(object {})
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
new file mode 100644
index 0000000..e6fc9ca
--- /dev/null
+++ 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt
@@ -0,0 +1,47 @@
+package tech.libeufin.nexus.server
+
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.request.*
+import io.ktor.util.*
+import io.ktor.util.pipeline.*
+import io.ktor.utils.io.*
+import io.ktor.utils.io.jvm.javaio.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.util.zip.InflaterInputStream
+
+/**
+ * Decompress request bodies.
+ */
+class RequestBodyDecompression private constructor() {
+    companion object Feature :
+        ApplicationFeature<Application, 
RequestBodyDecompression.Configuration, RequestBodyDecompression> {
+        override val key: AttributeKey<RequestBodyDecompression> = 
AttributeKey("Request Body Decompression")
+        override fun install(
+            pipeline: Application,
+            configure: RequestBodyDecompression.Configuration.() -> Unit
+        ): RequestBodyDecompression {
+            
pipeline.receivePipeline.intercept(ApplicationReceivePipeline.Before) {
+                if (this.context.request.headers["Content-Encoding"] == 
"deflate") {
+                    val deflated = this.subject.value as ByteReadChannel
+                    val brc = withContext(Dispatchers.IO) {
+                        val inflated = 
InflaterInputStream(deflated.toInputStream())
+                        // False positive in current Kotlin version, we're 
already in Dispatchers.IO!
+                        @Suppress("BlockingMethodInNonBlockingContext") val 
bytes = inflated.readAllBytes()
+                        ByteReadChannel(bytes)
+                    }
+                    
proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, brc))
+                    return@intercept
+                }
+                proceed()
+                return@intercept
+            }
+            return RequestBodyDecompression()
+        }
+    }
+
+    class Configuration {
+
+    }
+}
\ No newline at end of file
diff --git a/nexus/src/test/script/prepare_subscriber.sh 
b/nexus/src/test/script/prepare_subscriber.sh
deleted file mode 100755
index 7e354f9..0000000
--- a/nexus/src/test/script/prepare_subscriber.sh
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/bin/bash
-
-# This program allocates a new customer into the Sandbox
-# and Nexus systems.
-
-
-usage () {
-  printf "Usage: ./prepare_subscriber.sh <salt>\n"
-  printf "<salt> is any chars used to form user/partner/host IDs.\n"
-}
-
-continue_input () {
-  read -p "Continue? Press <Y/n> followed by <enter>" x
-  if test "$x" = "n"; then
-    printf "Aborting..\n"
-    exit
-  fi
-}
-
-exe_echo () {
-  echo \$ "$@"; "$@"
-}
-
-if ! which libeufin-cli > /dev/null; then
-  printf "Please make sure 'libeufin-cli' is in the PATH\n"
-  exit 1
-fi
-
-if [ -z "$1" ]; then
-  usage
-  exit 1
-fi
-
-ACCOUNT_ID="account-$1"
-BANK_BASE_URL="http://localhost:5000";
-NEXUS_BASE_URL="http://localhost:5001";
-
-printf "\nFirst: the new subscriber must exist at the Bank.\n"
-printf "For this reason, we invoke the \"admin\" part of its API.\n"
-printf "Press <enter> to proceed.."
-read x
-printf "\n"
-
-exe_echo libeufin-cli admin add-host \
-  --host-id "host$1" \
-  --ebics-version "2.5" $BANK_BASE_URL && sleep 1
-
-exe_echo libeufin-cli admin add-subscriber \
-  --user-id "user$1" \
-  --partner-id "partner$1" \
-  --host-id "host$1" \
-  --name "\"name $1\"" $BANK_BASE_URL && sleep 1
-
-continue_input
-
-printf "\nSecond: the Nexus must persist the same information\n"
-printf "and associate an alpha-numerical ID to it.\nPress <enter> to proceed.."
-read x
-
-printf "\n"
-exe_echo libeufin-cli ebics new-subscriber \
-  --account-id $ACCOUNT_ID \
-  --ebics-url http://localhost:5000/ebicsweb \
-  --user-id "user$1" \
-  --partner-id "partner$1" \
-  --host-id "host$1" $NEXUS_BASE_URL && sleep 1
-
-continue_input
-
-printf "Below are some common commands:\n"
-
-printf "\nSee again your account ID:\n"
-printf "\tlibeufin-cli ebics subscribers $NEXUS_BASE_URL\n"
-
-printf "Request INI, HIA, and HPB, with:\n"
-printf "\tlibeufin-cli ebics ini --account-id=$ACCOUNT_ID $NEXUS_BASE_URL\n"
-printf "\tlibeufin-cli ebics hia --account-id=$ACCOUNT_ID $NEXUS_BASE_URL\n"
-printf "\tlibeufin-cli ebics sync --account-id=$ACCOUNT_ID $NEXUS_BASE_URL\n\n"
diff --git a/util/src/main/kotlin/Errors.kt b/util/src/main/kotlin/Errors.kt
index e99e8be..d63d232 100644
--- a/util/src/main/kotlin/Errors.kt
+++ b/util/src/main/kotlin/Errors.kt
@@ -32,7 +32,7 @@ fun execThrowableOrTerminate(func: () -> Unit) {
     try {
         func()
     } catch (e: Exception) {
-        println(e.message)
+        e.printStackTrace()
         exitProcess(1)
     }
 }
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]