diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-05-19 18:05:33 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-05-19 18:05:33 +0530 |
commit | a43aa2e9e89281b27bbe823d0a9c3197a762c1d2 (patch) | |
tree | b7d46cf427dc7d1f8ca6620543df9d0a2be49848 | |
parent | 4dbd3ae0898c120666329b9b90edea7dc73e777d (diff) | |
download | libeufin-a43aa2e9e89281b27bbe823d0a9c3197a762c1d2.tar.gz libeufin-a43aa2e9e89281b27bbe823d0a9c3197a762c1d2.tar.bz2 libeufin-a43aa2e9e89281b27bbe823d0a9c3197a762c1d2.zip |
make tests pass
-rwxr-xr-x | integration-tests/test-ebics.py | 14 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 2 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 70 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 9 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 60 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 10 | ||||
-rw-r--r-- | util/src/test/kotlin/EbicsMessagesTest.kt | 1 |
7 files changed, 88 insertions, 78 deletions
diff --git a/integration-tests/test-ebics.py b/integration-tests/test-ebics.py index 50a217bf..f7a94bd4 100755 --- a/integration-tests/test-ebics.py +++ b/integration-tests/test-ebics.py @@ -5,7 +5,6 @@ from subprocess import call, Popen, PIPE from time import sleep import os import socket -import sqlite3 import hashlib import base64 @@ -89,9 +88,11 @@ assert(0 == call(["rm", "-f", "sandbox/libeufin-sandbox.sqlite3"])) assert(0 == call(["rm", "-f", "nexus/libeufin-nexus.sqlite3"])) DEVNULL = open(os.devnull, "w") +assert(0 == call(["./gradlew", "nexus:run", "--console=plain", "--args=superuser admin --password x"])) + # Start nexus checkPorts([5001]) -nexus = Popen(["./gradlew", "nexus:run"], stdout=PIPE, stderr=PIPE) +nexus = Popen(["./gradlew", "nexus:run", "--console=plain", "--args=serve"], stdout=PIPE, stderr=PIPE) for i in range(10): try: get("http://localhost:5001/") @@ -166,15 +167,6 @@ assertResponse( #1.a, make a new nexus user. -# "Create" the admin user first. -dbconn = sqlite3.connect("nexus/libeufin-nexus.sqlite3") -dbconn.execute( - "INSERT INTO NexusUsers (id, password) VALUES (?, ?)", - ("admin", sqlite3.Binary(hashlib.sha256(b"x").digest())) -) -dbconn.commit() -dbconn.close() - assertResponse( post( "http://localhost:5001/users", diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt index 9c7e225b..89d55e29 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -211,11 +211,13 @@ class EbicsSubscriberEntity(id: EntityID<String>) : Entity<String>(id) { object NexusUsersTable : IdTable<String>() { override val id = varchar("id", ID_MAX_LENGTH).entityId().primaryKey() val passwordHash = text("password") + val superuser = bool("superuser") } class NexusUserEntity(id: EntityID<String>) : Entity<String>(id) { companion object : EntityClass<String, NexusUserEntity>(NexusUsersTable) var passwordHash by NexusUsersTable.passwordHash + var superuser by NexusUsersTable.superuser } object BankAccountMapsTable : IntIdTable() { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt index ac77efb8..6258679d 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -7,18 +7,14 @@ import io.ktor.http.HttpStatusCode import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import org.joda.time.DateTime -import tech.libeufin.util.Amount -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.EbicsClientSubscriberDetails -import tech.libeufin.util.base64ToBytes -import java.util.Random +import tech.libeufin.util.* import tech.libeufin.util.ebics_h004.EbicsTypes import java.security.interfaces.RSAPublicKey -import tech.libeufin.util.* -import java.time.format.DateTimeFormatter -import java.time.ZonedDateTime import java.time.Instant import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.* fun isProduction(): Boolean { return System.getenv("NEXUS_PRODUCTION") != null @@ -75,8 +71,8 @@ fun getBankAccount(userId: String, accountId: String): BankAccountEntity { val bankAccountMap = BankAccountMapEntity.find { BankAccountMapsTable.nexusUser eq userId }.firstOrNull() ?: throw NexusError( - HttpStatusCode.NotFound, - "Bank account '$accountId' not found" + HttpStatusCode.NotFound, + "Bank account '$accountId' not found" ) bankAccountMap.bankAccount } @@ -142,7 +138,7 @@ fun getEbicsTransport(userId: String, transportId: String? = null): EbicsSubscri EbicsSubscribersTable.nexusUser eq userId }.firstOrNull() } - return@transaction EbicsSubscriberEntity.find{ + return@transaction EbicsSubscriberEntity.find { EbicsSubscribersTable.id eq transportId and (EbicsSubscribersTable.nexusUser eq userId) }.firstOrNull() } @@ -196,18 +192,23 @@ suspend fun downloadAndPersistC5xEbics( RawBankTransactionEntity.new { bankAccount = getBankAccountFromIban( camt53doc.pickString( - "//*[local-name()='Stmt']/*[local-name()='Acct']/*[local-name()='Id']/*[local-name()='IBAN']") + "//*[local-name()='Stmt']/*[local-name()='Acct']/*[local-name()='Id']/*[local-name()='IBAN']" + ) ) sourceFileName = fileName - unstructuredRemittanceInformation = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Ustrd']") + unstructuredRemittanceInformation = + camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Ustrd']") transactionType = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='CdtDbtInd']") currency = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy") amount = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']") status = camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']") - bookingDate = parseDashedDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis + bookingDate = + parseDashedDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis nexusUser = extractNexusUser(userId) - counterpartIban = camt53doc.pickString("//*[local-name()='${if (this.transactionType == "DBIT") "CdtrAcct" else "DbtrAcct"}']//*[local-name()='IBAN']") - counterpartName = camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='${if (this.transactionType == "DBIT") "Cdtr" else "Dbtr"}']//*[local-name()='Nm']") + counterpartIban = + camt53doc.pickString("//*[local-name()='${if (this.transactionType == "DBIT") "CdtrAcct" else "DbtrAcct"}']//*[local-name()='IBAN']") + counterpartName = + camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='${if (this.transactionType == "DBIT") "Cdtr" else "Dbtr"}']//*[local-name()='Nm']") counterpartBic = camt53doc.pickString("//*[local-name()='RltdAgts']//*[local-name()='BIC']") } } @@ -217,7 +218,7 @@ suspend fun downloadAndPersistC5xEbics( throw NexusError( HttpStatusCode.BadGateway, response.returnCode.errorCode - ) + ) } } } @@ -426,7 +427,7 @@ fun extractNexusUser(param: String?): NexusUserEntity { if (param == null) { throw NexusError(HttpStatusCode.BadRequest, "Null Id given") } - return transaction{ + return transaction { NexusUserEntity.findById(param) ?: throw NexusError( HttpStatusCode.NotFound, "Subscriber: $param not found" @@ -461,34 +462,23 @@ fun extractUserAndHashedPassword(authorizationHeader: String): Pair<String, Stri * @param authorization the Authorization:-header line. * @return user id */ -fun authenticateRequest(authorization: String?): String { +fun authenticateRequest(authorization: String?): NexusUserEntity { val headerLine = if (authorization == null) throw NexusError( HttpStatusCode.BadRequest, "Authentication:-header line not found" ) else authorization - val nexusUserId = transaction { - val (username, password) = extractUserAndHashedPassword(headerLine) - val user = NexusUserEntity.find { - NexusUsersTable.id eq username - }.firstOrNull() - if (user == null) { - throw NexusError(HttpStatusCode.Unauthorized, "Unknown user") - } - if (!CryptoUtil.checkpw(password, user.passwordHash)) { - throw NexusError(HttpStatusCode.Forbidden, "Wrong password") - } - return@transaction user.id.value + val (username, password) = extractUserAndHashedPassword(headerLine) + val user = NexusUserEntity.find { + NexusUsersTable.id eq username + }.firstOrNull() + if (user == null) { + throw NexusError(HttpStatusCode.Unauthorized, "Unknown user") } - return nexusUserId + if (!CryptoUtil.checkpw(password, user.passwordHash)) { + throw NexusError(HttpStatusCode.Forbidden, "Wrong password") + } + return user } -fun authenticateAdminRequest(authorization: String?): String { - val userId = authenticateRequest(authorization) - if (!userId.equals("admin")) throw NexusError( - HttpStatusCode.Forbidden, - "Not the 'admin' user" - ) - return userId -} /** * Check if the subscriber has the right to use the (claimed) bank account. diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt index 7b764f2a..d48e283e 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt @@ -190,6 +190,15 @@ data class User( val password: String ) +data class UserInfo( + val username: String, + val superuser: Boolean +) + +data class UsersResponse( + val users: List<UserInfo> +) + /** Response (list's element) type of "GET /bank-accounts" */ data class BankAccount( var holder: String, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt index ec2080e5..5bcf1008 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -20,6 +20,7 @@ package tech.libeufin.nexus import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.ProgramResult import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.options.option @@ -174,8 +175,8 @@ class Serve: CliktCommand("Run nexus HTTP server") { } class Superuser: CliktCommand("Add superuser or change pw") { - val username by argument() - val password by option().prompt(requireConfirmation = true, hideInput = true) + private val username by argument() + private val password by option().prompt(requireConfirmation = true, hideInput = true) override fun run() { dbCreateTables() transaction { @@ -184,8 +185,13 @@ class Superuser: CliktCommand("Add superuser or change pw") { if (user == null) { NexusUserEntity.new(username) { this.passwordHash = hashedPw + this.superuser = true } } else { + if (!user.superuser) { + println("Can only change password for superuser with this command.") + throw ProgramResult(1) + } user.passwordHash = hashedPw } } @@ -267,30 +273,42 @@ fun serverMain() { * Shows information about the requesting user. */ get("/user") { - val userId = authenticateRequest(call.request.headers["Authorization"]) val ret = transaction { - NexusUserEntity.findById(userId) + val currentUser = authenticateRequest(call.request.headers["Authorization"]) UserResponse( - username = userId, - superuser = userId.equals("admin") + username = currentUser.id.value, + superuser = currentUser.superuser ) } call.respond(HttpStatusCode.OK, ret) return@get } + + get("/users") { + val users = transaction { + transaction { + NexusUserEntity.all().map { + UserInfo(it.id.value, it.superuser) + } + } + } + val usersResp = UsersResponse(users) + call.respond(HttpStatusCode.OK, usersResp) + return@get + } /** - * Add a new ordinary user in the system (requires "admin" privileges) + * Add a new ordinary user in the system (requires superuser privileges) */ post("/users") { - authenticateAdminRequest(call.request.headers["Authorization"]) val body = call.receive<User>() - if (body.username.equals("admin")) throw NexusError( - HttpStatusCode.Forbidden, - "'admin' is a reserved username" - ) transaction { + val currentUser = authenticateRequest(call.request.headers["Authorization"]) + if (!currentUser.superuser) { + throw NexusError(HttpStatusCode.Forbidden, "only superuser can do that") + } NexusUserEntity.new(body.username) { passwordHash = hashpw(body.password) + superuser = false } } call.respondText( @@ -304,7 +322,7 @@ fun serverMain() { * Shows the bank accounts belonging to the requesting user. */ get("/bank-accounts") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val bankAccounts = BankAccounts() getBankAccountsFromNexusUserId(userId).forEach { bankAccounts.accounts.add( @@ -322,7 +340,7 @@ fun serverMain() { * Submit one particular payment at the bank. */ post("/bank-accounts/prepared-payments/submit") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val body = call.receive<SubmitPayment>() val preparedPayment = getPreparedPayment(body.uuid) transaction { @@ -368,7 +386,7 @@ fun serverMain() { * Shows information about one particular prepared payment. */ get("/bank-accounts/{accountid}/prepared-payments/{uuid}") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val preparedPayment = getPreparedPayment(ensureNonNull(call.parameters["uuid"])) if (preparedPayment.nexusUser.id.value != userId) throw NexusError( HttpStatusCode.Forbidden, @@ -393,7 +411,7 @@ fun serverMain() { * Adds a new prepared payment. */ post("/bank-accounts/{accountid}/prepared-payments") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val bankAccount = getBankAccount(userId, ensureNonNull(call.parameters["accountid"])) val body = call.receive<PreparedPaymentRequest>() val amount = parseAmount(body.amount) @@ -423,7 +441,7 @@ fun serverMain() { * bank account details) */ post("/bank-accounts/collected-transactions") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val body = call.receive<CollectedTransaction>() if (body.transport != null) { when (body.transport.type) { @@ -459,7 +477,7 @@ fun serverMain() { * Asks list of transactions ALREADY downloaded from the bank. */ get("/bank-accounts/{accountid}/collected-transactions") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val bankAccount = expectNonNull(call.parameters["accountid"]) val start = call.request.queryParameters["start"] val end = call.request.queryParameters["end"] @@ -493,7 +511,7 @@ fun serverMain() { * Adds a new bank transport. */ post("/bank-transports") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } // user exists and is authenticated. val body = call.receive<JsonObject>() val transport: Transport = getTransportFromJsonObject(body) @@ -594,7 +612,7 @@ fun serverMain() { * "transportName". Does not modify any DB table. */ post("/bank-transports/send{MSG}") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val body = call.receive<Transport>() when (body.type) { "ebics" -> { @@ -619,7 +637,7 @@ fun serverMain() { * "transportName". DOES alterate DB tables. */ post("/bank-transports/sync{MSG}") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val body = call.receive<Transport>() when (body.type) { "ebics" -> { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt index f3dd6148..2b6b3ffc 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt @@ -220,7 +220,7 @@ class Taler(app: Route) { return@get } app.post("/taler/transfer") { - val exchangeId = authenticateRequest(call.request.headers["Authorization"]) + val exchangeId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val transferRequest = call.receive<TalerTransferRequest>() val amountObj = parseAmount(transferRequest.amount) val creditorObj = parsePayto(transferRequest.credit_account) @@ -304,7 +304,7 @@ class Taler(app: Route) { } /** Test-API that creates one new payment addressed to the exchange. */ app.post("/taler/admin/add-incoming") { - val exchangeId = authenticateRequest(call.request.headers["Authorization"]) + val exchangeId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val addIncomingData = call.receive<TalerAdminAddIncoming>() val debtor = parsePayto(addIncomingData.debit_account) val amount = parseAmount(addIncomingData.amount) @@ -351,7 +351,7 @@ class Taler(app: Route) { * places it into a further table. Eventually, another routine will perform * all the prepared payments. */ app.post("/ebics/taler/{id}/accounts/{acctid}/refund-invalid-payments") { - val userId = authenticateRequest(call.request.headers["Authorization"]) + val userId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val nexusUser = getNexusUser(userId) val callerBankAccount = expectNonNull(call.parameters["acctid"]) transaction { @@ -477,7 +477,7 @@ class Taler(app: Route) { */ app.get("/taler/history/outgoing") { /* sanitize URL arguments */ - val subscriberId = authenticateRequest(call.request.headers["Authorization"]) + val subscriberId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val delta: Int = expectInt(call.expectUrlParameter("delta")) val start: Long = handleStartArgument(call.request.queryParameters["start"], delta) val startCmpOp = getComparisonOperator(delta, start, TalerRequestedPayments) @@ -514,7 +514,7 @@ class Taler(app: Route) { } /** Responds only with the valid incoming payments */ app.get("/taler/history/incoming") { - val exchangeId = authenticateRequest(call.request.headers["Authorization"]) + val exchangeId = transaction { authenticateRequest(call.request.headers["Authorization"]).id.value } val delta: Int = expectInt(call.expectUrlParameter("delta")) val start: Long = handleStartArgument(call.request.queryParameters["start"], delta) val history = TalerIncomingHistory() diff --git a/util/src/test/kotlin/EbicsMessagesTest.kt b/util/src/test/kotlin/EbicsMessagesTest.kt index e7f2cf1f..4c0032c8 100644 --- a/util/src/test/kotlin/EbicsMessagesTest.kt +++ b/util/src/test/kotlin/EbicsMessagesTest.kt @@ -10,7 +10,6 @@ import tech.libeufin.util.ebics_hev.SystemReturnCodeType import tech.libeufin.util.ebics_s001.SignatureTypes import tech.libeufin.util.CryptoUtil import tech.libeufin.util.XMLUtil -import tech.libeufin.util.ebics_h004.* import javax.xml.datatype.DatatypeFactory import kotlin.test.assertNotNull import kotlin.test.assertTrue |