diff options
author | Florian Dold <florian@dold.me> | 2021-01-20 20:32:29 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2021-01-20 20:32:29 +0100 |
commit | ceab4b823f98ea34fdec46784537ad120ae50e73 (patch) | |
tree | 7397b2d396a01e685f40f31012c9733450250042 /nexus | |
parent | ccf1edff26adc9f4b4a9a16856ab1347f6120205 (diff) | |
download | libeufin-ceab4b823f98ea34fdec46784537ad120ae50e73.tar.gz libeufin-ceab4b823f98ea34fdec46784537ad120ae50e73.tar.bz2 libeufin-ceab4b823f98ea34fdec46784537ad120ae50e73.zip |
rudimentary permissions, code cleanup
Diffstat (limited to 'nexus')
-rw-r--r-- | nexus/build.gradle | 5 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt | 116 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 84 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 1 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 65 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt | 49 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt | 206 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/server/RequestBodyDecompression.kt | 47 |
8 files changed, 393 insertions, 180 deletions
diff --git a/nexus/build.gradle b/nexus/build.gradle index e048a85b..e46e6cf6 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 00000000..cc371a12 --- /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 c164aee8..c51926eb 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 82c3efe1..2d17b8ef 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 ddf67979..9e6f6fd1 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 fa827f82..23dccf31 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 3c8d06bc..8c65427a 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 00000000..e6fc9cac --- /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 |