From 24e078e9db5f57802d7b2ce94d74def0f930baca Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 21 Jan 2021 12:34:41 +0100 Subject: restrict resource names, cleanup --- .idea/dictionaries/dold.xml | 1 + cli/bin/libeufin-cli | 30 ++++++++++--- .../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 2 +- .../tech/libeufin/nexus/server/NexusServer.kt | 50 +++++++++++++++------- .../src/main/kotlin/tech/libeufin/sandbox/DB.kt | 4 +- .../tech/libeufin/sandbox/EbicsProtocolBackend.kt | 28 ++++++------ .../src/main/kotlin/tech/libeufin/sandbox/Main.kt | 16 +++---- util/src/test/kotlin/XmlUtilTest.kt | 2 +- 8 files changed, 83 insertions(+), 50 deletions(-) diff --git a/.idea/dictionaries/dold.xml b/.idea/dictionaries/dold.xml index 12a9811b..48c7f058 100644 --- a/.idea/dictionaries/dold.xml +++ b/.idea/dictionaries/dold.xml @@ -11,6 +11,7 @@ gnunet iban infos + keyletter libeufin payto pdng diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli index 5e89e1e8..c7aabe54 100755 --- a/cli/bin/libeufin-cli +++ b/cli/bin/libeufin-cli @@ -359,7 +359,7 @@ def restore_backup(obj, backup_file, passphrase, connection_name): tell_user(resp) -@connections.command(help="make new Ebics bank connection") +@connections.command(help="make new EBICS bank connection") @click.option("--ebics-url", help="EBICS URL", required=True) @click.option("--host-id", help="Host ID", required=True) @click.option("--partner-id", help="Partner ID", required=True) @@ -390,10 +390,10 @@ def new_ebics_connection( tell_user(resp) -@connections.command(help="synchronize the bank connection") +@connections.command(help="Initialize the bank connection.") @click.argument("connection-name") @click.pass_obj -def sync(obj, connection_name): +def connect(obj, connection_name): url = urljoin(obj.nexus_base_url, f"/bank-connections/{connection_name}/connect") try: resp = post( @@ -405,7 +405,7 @@ def sync(obj, connection_name): tell_user(resp) -@connections.command(help="import one bank account, chosen from the downloaded ones") +@connections.command(help="Import one bank account, chosen from the downloaded ones.") @click.option( "--offered-account-id", help="Name of the account to import", required=True ) @@ -439,7 +439,7 @@ def import_bank_account( tell_user(resp) -@connections.command(help="download bank accounts in raw format WITHOUT importing them") +@connections.command(help="Update list of bank accounts available through this connection.") @click.argument("connection-name") @click.pass_obj def download_bank_accounts(obj, connection_name): @@ -458,10 +458,26 @@ def download_bank_accounts(obj, connection_name): tell_user(resp) -@connections.command(help="list the connections") +@connections.command(help="List the connections.") @click.pass_obj def list_connections(obj): - url = urljoin(obj.nexus_base_url, "/bank-connections/") + url = urljoin(obj.nexus_base_url, "/bank-connections") + try: + resp = get( + url, json=dict(), auth=auth.HTTPBasicAuth(obj.username, obj.password) + ) + except Exception: + print("Could not reach nexus at " + url) + exit(1) + + tell_user(resp, withsuccess=True) + + +@connections.command(help="Show the status of a bank connection.") +@click.argument("connection-name") +@click.pass_obj +def show_connection(obj, connection_name): + url = urljoin(obj.nexus_base_url, f"/bank-connections/{connection_name}") try: resp = get( url, json=dict(), auth=auth.HTTPBasicAuth(obj.username, obj.password) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt index 6afeb711..2f2791b8 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -592,7 +592,7 @@ fun getEbicsConnectionDetails(conn: NexusBankConnectionEntity): Any { details.put("userId", ebicsSubscriber.userId) val node = mapper.createObjectNode() node.put("type", conn.type) - node.put("owner", conn.owner.id.value) + node.put("owner", conn.owner.username) node.set("details", details) return node } 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 79fb3340..7d4e368e 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt @@ -121,6 +121,22 @@ fun ApplicationCall.expectUrlParameter(name: String): String { ?: throw NexusError(HttpStatusCode.BadRequest, "Parameter '$name' not provided in URI") } +fun isValidResourceName(name: String): Boolean { + return name.matches(Regex("[a-z]([-a-z0-9]*[a-z0-9])?")) +} + +fun requireValidResourceName(name: String): String { + if (!isValidResourceName(name)) { + throw NexusError( + HttpStatusCode.BadRequest, + "Invalid resource name. The first character must be a lowercase letter, " + + "and all following characters (except for the last character) must be a dash, " + + "lowercase letter, or digit. The last character must be a lowercase letter or digit." + ) + } + return name +} + suspend inline fun ApplicationCall.receiveJson(): T { try { return this.receive() @@ -326,10 +342,11 @@ 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() + val requestedUsername = requireValidResourceName(body.username) transaction { requireSuperuser(call.request) NexusUserEntity.new { - username = body.username + username = requestedUsername passwordHash = CryptoUtil.hashpw(body.password) superuser = false } @@ -448,7 +465,7 @@ fun serverMain(dbName: String, host: String, port: Int) { resourceType = "bank-account" resourceId = accountId this.taskCronspec = schedSpec.cronspec - this.taskName = schedSpec.name + this.taskName = requireValidResourceName(schedSpec.name) this.taskType = schedSpec.type this.taskParams = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(schedSpec.params) @@ -527,8 +544,7 @@ fun serverMain(dbName: String, host: String, port: Int) { 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 { + transaction { authenticateRequest(call.request) } submitPaymentInitiation(client, uuid) @@ -539,7 +555,7 @@ fun serverMain(dbName: String, host: String, port: Int) { post("/bank-accounts/{accountid}/submit-all-payment-initiations") { requireSuperuser(call.request) val accountId = ensureNonNull(call.parameters["accountid"]) - val res = transaction { + transaction { authenticateRequest(call.request) } submitAllPaymentInitiations(client, accountId) @@ -698,6 +714,7 @@ fun serverMain(dbName: String, host: String, port: Int) { requireSuperuser(call.request) // user exists and is authenticated. val body = call.receive() + requireValidResourceName(body.name) transaction { val user = authenticateRequest(call.request) val existingConn = @@ -774,11 +791,11 @@ fun serverMain(dbName: String, host: String, port: Int) { call.respond(connList) } - get("/bank-connections/{connectionId}") { + get("/bank-connections/{connectionName}") { requireSuperuser(call.request) val resp = transaction { val user = authenticateRequest(call.request) - val conn = requireBankConnection(call, "connectionId") + val conn = requireBankConnection(call, "connectionName") when (conn.type) { "ebics" -> { getEbicsConnectionDetails(conn) @@ -794,12 +811,12 @@ fun serverMain(dbName: String, host: String, port: Int) { call.respond(resp) } - post("/bank-connections/{connid}/export-backup") { + post("/bank-connections/{connectionName}/export-backup") { requireSuperuser(call.request) transaction { authenticateRequest(call.request) } val body = call.receive() val response = run { - val conn = requireBankConnection(call, "connid") + val conn = requireBankConnection(call, "connectionName") when (conn.type) { "ebics" -> { exportEbicsKeyBackup(conn.connectionId, body.passphrase) @@ -819,11 +836,11 @@ fun serverMain(dbName: String, host: String, port: Int) { ) } - post("/bank-connections/{connid}/connect") { + post("/bank-connections/{connectionName}/connect") { requireSuperuser(call.request) val conn = transaction { authenticateRequest(call.request) - requireBankConnection(call, "connid") + requireBankConnection(call, "connectionName") } when (conn.type) { "ebics" -> { @@ -833,11 +850,11 @@ fun serverMain(dbName: String, host: String, port: Int) { call.respond(object {}) } - get("/bank-connections/{connid}/keyletter") { + get("/bank-connections/{connectionName}/keyletter") { requireSuperuser(call.request) val conn = transaction { authenticateRequest(call.request) - requireBankConnection(call, "connid") + requireBankConnection(call, "connectionName") } when (conn.type) { "ebics" -> { @@ -848,11 +865,11 @@ fun serverMain(dbName: String, host: String, port: Int) { } } - get("/bank-connections/{connid}/messages") { + get("/bank-connections/{connectionName}/messages") { requireSuperuser(call.request) val ret = transaction { val list = BankMessageList() - val conn = requireBankConnection(call, "connid") + val conn = requireBankConnection(call, "connectionName") NexusBankMessageEntity.find { NexusBankMessagesTable.bankConnection eq conn.id }.map { list.bankMessages.add( BankMessageInfo( @@ -928,6 +945,7 @@ fun serverMain(dbName: String, host: String, port: Int) { post("/facades") { requireSuperuser(call.request) val body = call.receive() + requireValidResourceName(body.name) if (body.type != "taler-wire-gateway") throw NexusError( HttpStatusCode.NotImplemented, "Facade type '${body.type}' is not implemented" @@ -1025,6 +1043,6 @@ fun serverMain(dbName: String, host: String, port: Int) { } } } - logger.info("Up and running") + logger.info("LibEuFin Nexus running on port $port") server.start(wait = true) } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt index d0b024b1..c6add963 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -313,7 +313,7 @@ object BankAccountReportsTable : IntIdTable() { } fun dbDropTables(dbConnectionString: String) { - Database.connect("${dbConnectionString}") + Database.connect(dbConnectionString) transaction { SchemaUtils.drop( EbicsSubscribersTable, @@ -331,7 +331,7 @@ fun dbDropTables(dbConnectionString: String) { } fun dbCreateTables(dbConnectionString: String) { - Database.connect("${dbConnectionString}") + Database.connect(dbConnectionString) TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE transaction { SchemaUtils.create( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt index 0e268717..e06fd26b 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -145,7 +145,7 @@ private suspend fun ApplicationCall.respondEbicsKeyManagement( } } val text = XMLUtil.convertJaxbToString(responseXml) - LOGGER.info("responding with:\n${text}") + logger.info("responding with:\n${text}") if (!XMLUtil.validateFromString(text)) throw SandboxError( HttpStatusCode.InternalServerError, "Outgoint EBICS key management response is invalid" @@ -548,8 +548,8 @@ private fun parsePain001(paymentRequest: String): PainParseResult { * Process a payment request in the pain.001 format. */ private fun handleCct(paymentRequest: String, initiatorName: String) { - LOGGER.debug("Handling CCT") - LOGGER.debug("Pain.001: $paymentRequest") + logger.debug("Handling CCT") + logger.debug("Pain.001: $paymentRequest") val parseResult = parsePain001(paymentRequest) transaction { try { @@ -580,7 +580,7 @@ private fun handleCct(paymentRequest: String, initiatorName: String) { } private fun handleEbicsC53(requestContext: RequestContext): ByteArray { - LOGGER.debug("Handling C53 request") + logger.debug("Handling C53 request") val camt = constructCamtResponse( 53, requestContext.requestObject.header, @@ -608,7 +608,7 @@ private suspend fun ApplicationCall.handleEbicsHia(header: EbicsUnsecuredRequest val ok = transaction { val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) if (ebicsSubscriber == null) { - LOGGER.warn("ebics subscriber not found") + logger.warn("ebics subscriber not found") throw EbicsInvalidRequestError() } when (ebicsSubscriber.state) { @@ -655,7 +655,7 @@ private suspend fun ApplicationCall.handleEbicsIni(header: EbicsUnsecuredRequest val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) if (ebicsSubscriber == null) { - LOGGER.warn("ebics subscriber ('${header.static.partnerID}' / '${header.static.userID}' / '${header.static.systemID}') not found") + logger.warn("ebics subscriber ('${header.static.partnerID}' / '${header.static.userID}' / '${header.static.systemID}') not found") throw EbicsInvalidRequestError() } when (ebicsSubscriber.state) { @@ -676,7 +676,7 @@ private suspend fun ApplicationCall.handleEbicsIni(header: EbicsUnsecuredRequest } return@transaction true } - LOGGER.info("Signature key inserted in database _and_ subscriber state changed accordingly") + logger.info("Signature key inserted in database _and_ subscriber state changed accordingly") if (ok) { respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000") } else { @@ -709,7 +709,7 @@ private suspend fun ApplicationCall.handleEbicsHpb( } val validationResult = XMLUtil.verifyEbicsDocument(requestDocument, subscriberKeys.authenticationPublicKey) - LOGGER.info("validationResult: $validationResult") + logger.info("validationResult: $validationResult") if (!validationResult) { throw EbicsKeyManagementError("invalid signature", "90000") } @@ -750,7 +750,7 @@ private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPub val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq requestHostID.toUpperCase() }.firstOrNull() if (ebicsHost == null) { - LOGGER.warn("client requested unknown HostID ${requestHostID}") + logger.warn("client requested unknown HostID ${requestHostID}") throw EbicsKeyManagementError("[EBICS_INVALID_HOST_ID]", "091011") } val encryptionPrivateKey = CryptoUtil.loadRsaPrivateKey(ebicsHost.encryptionPrivateKey.bytes) @@ -765,7 +765,7 @@ private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPub private suspend fun ApplicationCall.receiveEbicsXml(): Document { val body: String = receiveText() - LOGGER.debug("Data received: $body") + logger.debug("Data received: $body") val requestDocument: Document? = XMLUtil.parseStringIntoDom(body) if (requestDocument == null || (!XMLUtil.validateFromDom(requestDocument))) { println("Problematic document was: $requestDocument") @@ -1136,11 +1136,11 @@ private fun makeRequestContext(requestObject: EbicsRequest): RequestContext { suspend fun ApplicationCall.ebicsweb() { val requestDocument = receiveEbicsXml() - LOGGER.info("Processing ${requestDocument.documentElement.localName}") + logger.info("Processing ${requestDocument.documentElement.localName}") when (requestDocument.documentElement.localName) { "ebicsUnsecuredRequest" -> { val requestObject = requestDocument.toObject() - LOGGER.info("Serving a ${requestObject.header.static.orderDetails.orderType} request") + logger.info("Serving a ${requestObject.header.static.orderDetails.orderType} request") val orderData = requestObject.body.dataTransfer.orderData.value val header = requestObject.header @@ -1161,7 +1161,7 @@ suspend fun ApplicationCall.ebicsweb() { } val strResp = XMLUtil.convertJaxbToString(hevResponse) - LOGGER.debug("HEV response: $strResp") + logger.debug("HEV response: $strResp") if (!XMLUtil.validateFromString(strResp)) throw SandboxError( HttpStatusCode.InternalServerError, "Outgoing HEV response is invalid" @@ -1225,7 +1225,7 @@ suspend fun ApplicationCall.ebicsweb() { } else -> { /* Log to console and return "unknown type" */ - LOGGER.info("Unknown message, just logging it!") + logger.info("Unknown message, just logging it!") respond( HttpStatusCode.NotImplemented, SandboxError( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt index 87ad4746..0653494b 100644 --- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt +++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -60,7 +60,6 @@ import com.github.ajalt.clikt.core.context import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.output.CliktHelpFormatter import com.github.ajalt.clikt.parameters.options.default -import com.github.ajalt.clikt.parameters.options.flag import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.versionOption import com.github.ajalt.clikt.parameters.types.int @@ -83,7 +82,6 @@ import tech.libeufin.util.ebics_h004.EbicsResponse import tech.libeufin.util.ebics_h004.EbicsTypes import java.util.* import kotlin.random.Random -import kotlin.system.exitProcess const val DEFAULT_DB_CONNECTION = "jdbc:sqlite:/tmp/libeufin-sandbox.sqlite3" @@ -93,7 +91,7 @@ class UnacceptableFractional(badNumber: BigDecimal) : Exception( "Unacceptable fractional part ${badNumber}" ) -lateinit var LOGGER: Logger +private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") data class SandboxError(val statusCode: HttpStatusCode, val reason: String) : Exception() data class SandboxErrorJson(val error: SandboxErrorDetailJson) @@ -126,7 +124,6 @@ class Serve : CliktCommand("Run sandbox HTTP server") { private val logLevel by option() private val port by option().int().default(5000) override fun run() { - LOGGER = LoggerFactory.getLogger("tech.libeufin.sandbox") setLogLevel(logLevel) serverMain(dbConnString, port) } @@ -207,10 +204,11 @@ fun main(args: Array) { fun serverMain(dbName: String, port: Int) { execThrowableOrTerminate { dbCreateTables(dbName) } + val myLogger = logger val server = embeddedServer(Netty, port = port) { install(CallLogging) { this.level = Level.DEBUG - this.logger = LOGGER + this.logger = myLogger } install(ContentNegotiation) { jackson { @@ -225,7 +223,7 @@ fun serverMain(dbName: String, port: Int) { } install(StatusPages) { exception { cause -> - LOGGER.error("Exception while handling '${call.request.uri}'", cause) + logger.error("Exception while handling '${call.request.uri}'", cause) call.respondText( "Invalid arithmetic attempted.", ContentType.Text.Plain, @@ -261,7 +259,7 @@ fun serverMain(dbName: String, port: Int) { ) } exception { cause -> - LOGGER.error("Exception while handling '${call.request.uri}'", cause) + logger.error("Exception while handling '${call.request.uri}'", cause) call.respond( cause.statusCode, SandboxErrorJson( @@ -273,7 +271,7 @@ fun serverMain(dbName: String, port: Int) { ) } exception { cause -> - LOGGER.error("Exception while handling '${call.request.uri}'", cause) + logger.error("Exception while handling '${call.request.uri}'", cause) call.respondText("Internal server error.", ContentType.Text.Plain, HttpStatusCode.InternalServerError) } } @@ -566,6 +564,6 @@ fun serverMain(dbName: String, port: Int) { } } } - LOGGER.info("Up and running") + logger.info("LibEuFin Nexus running on port $port") server.start(wait = true) } \ No newline at end of file diff --git a/util/src/test/kotlin/XmlUtilTest.kt b/util/src/test/kotlin/XmlUtilTest.kt index 57394b8b..32b91a23 100644 --- a/util/src/test/kotlin/XmlUtilTest.kt +++ b/util/src/test/kotlin/XmlUtilTest.kt @@ -14,7 +14,7 @@ import java.util.* import javax.xml.transform.stream.StreamSource import tech.libeufin.util.XMLUtil.Companion.signEbicsResponse -val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util") +private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util.XmlUnitTest") class XmlUtilTest { -- cgit v1.2.3