libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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

rudimentary permissions, code cleanup

Diffstat:
M.idea/dictionaries/dold.xml | 3+++
M.idea/inspectionProfiles/Project_Default.xml | 1+
Mbuild.gradle | 2+-
Mcli/bin/libeufin-cli | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcli/setup-template.sh | 6+++---
Mnexus/build.gradle | 5++---
Anexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 1-
Mnexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 65+++++++++++++++++++++++++++++++++--------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt | 49+++++++++++++++++++++++++++----------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt | 206+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Anexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mutil/src/main/kotlin/Errors.kt | 2+-
14 files changed, 606 insertions(+), 207 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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 @@ -67,6 +64,132 @@ def connections(ctx): @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): ctx.obj = NexusAccess(*fetch_env()) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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