gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] 03/03: rudimentary permissions, code cleanup


From: gnunet
Subject: [libeufin] 03/03: rudimentary permissions, code cleanup
Date: Wed, 20 Jan 2021 20:32:36 +0100

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

dold pushed a commit to branch master
in repository libeufin.

commit ceab4b823f98ea34fdec46784537ad120ae50e73
Author: Florian Dold <florian@dold.me>
AuthorDate: Wed Jan 20 20:32:29 2021 +0100

    rudimentary permissions, code cleanup
---
 .idea/dictionaries/dold.xml                        |   3 +
 .idea/inspectionProfiles/Project_Default.xml       |   1 +
 build.gradle                                       |   2 +-
 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 +++++
 util/src/main/kotlin/Errors.kt                     |   2 +-
 14 files changed, 604 insertions(+), 207 deletions(-)

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/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/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]