gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated: Provide authentication to Sandbox


From: gnunet
Subject: [libeufin] branch master updated: Provide authentication to Sandbox
Date: Fri, 17 Sep 2021 15:46:47 +0200

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

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 42c62f2  Provide authentication to Sandbox
42c62f2 is described below

commit 42c62f26b4fe0e5cc2fc3f29f8b79f25b2c49ee0
Author: ms <ms@taler.net>
AuthorDate: Fri Sep 17 15:46:37 2021 +0200

    Provide authentication to Sandbox
---
 nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt  | 44 ++++-----------
 .../src/main/kotlin/tech/libeufin/sandbox/Auth.kt  | 45 ++++++++++++++++
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  | 63 ++++++++++++++++++++++
 util/src/main/kotlin/CryptoUtil.kt                 | 14 +++++
 util/src/main/kotlin/EbicsOrderUtil.kt             |  2 +-
 util/src/main/kotlin/Errors.kt                     |  7 ++-
 util/src/main/kotlin/HTTP.kt                       | 43 +++++++++++++++
 util/src/main/kotlin/LibeufinErrorCodes.kt         | 14 ++++-
 util/src/main/kotlin/XMLUtil.kt                    |  3 +-
 util/src/main/kotlin/strings.kt                    |  6 +--
 10 files changed, 198 insertions(+), 43 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
index 9d222e9..639abb3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
@@ -1,5 +1,6 @@
 package tech.libeufin.nexus
 
+import UtilError
 import io.ktor.application.*
 import io.ktor.http.*
 import io.ktor.request.*
@@ -7,54 +8,27 @@ 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
-
+import tech.libeufin.util.*
 
 /**
- * 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,
+ * 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 (username, password) = getHTTPBasicAuthCredentials(request)
         val user = NexusUserEntity.find {
             NexusUsersTable.username 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")
+            throw UtilError(HttpStatusCode.Unauthorized,
+                "Unknown user '$username'",
+                LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
+            )
         }
+        CryptoUtil.checkPwOrThrow(password, username)
         user
     }
 }
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Auth.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Auth.kt
new file mode 100644
index 0000000..b86bdf7
--- /dev/null
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Auth.kt
@@ -0,0 +1,45 @@
+package tech.libeufin.sandbox
+
+import UtilError
+import io.ktor.http.*
+import io.ktor.request.*
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.LibeufinErrorCode
+import tech.libeufin.util.getHTTPBasicAuthCredentials
+
+
+/**
+ * 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): SandboxUserEntity {
+    return transaction {
+        val (username, password) = getHTTPBasicAuthCredentials(request)
+        val user = SandboxUserEntity.find {
+            SandboxUsersTable.username eq username
+        }.firstOrNull()
+        if (user == null) {
+            throw UtilError(
+                HttpStatusCode.Unauthorized,
+                "Unknown user '$username'",
+                LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
+            )
+        }
+        CryptoUtil.checkPwOrThrow(password, username)
+        user
+    }
+}
+
+fun requireSuperuser(request: ApplicationRequest): SandboxUserEntity {
+    return transaction {
+        val user = authenticateRequest(request)
+        if (!user.superuser) {
+            throw SandboxError(HttpStatusCode.Forbidden, "must be superuser")
+        }
+        user
+    }
+}
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index 8c54cef..4a01d5f 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -57,6 +57,7 @@ import 
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
 import org.jetbrains.exposed.sql.statements.api.ExposedBlob
 import java.time.Instant
 import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.core.ProgramResult
 import com.github.ajalt.clikt.core.context
 import com.github.ajalt.clikt.parameters.arguments.argument
 import com.github.ajalt.clikt.core.subcommands
@@ -71,6 +72,7 @@ import io.ktor.http.*
 import io.ktor.http.content.*
 import io.ktor.request.*
 import io.ktor.util.date.*
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
 import tech.libeufin.util.*
 import tech.libeufin.util.ebics_h004.EbicsResponse
 import tech.libeufin.util.ebics_h004.EbicsTypes
@@ -92,6 +94,33 @@ data class SandboxError(
 data class SandboxErrorJson(val error: SandboxErrorDetailJson)
 data class SandboxErrorDetailJson(val type: String, val description: String)
 
+class Superuser : CliktCommand("Add superuser or change pw") {
+    private val username by argument()
+    private val password by option().prompt(requireConfirmation = true, 
hideInput = true)
+    override fun run() {
+        execThrowableOrTerminate {
+            dbCreateTables(getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME))
+        }
+        transaction {
+            val hashedPw = CryptoUtil.hashpw(password)
+            val user = SandboxUserEntity.find { SandboxUsersTable.username eq 
username }.firstOrNull()
+            if (user == null) {
+                SandboxUserEntity.new {
+                    this.username = this@Superuser.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
+            }
+        }
+    }
+}
+
 class Config : CliktCommand("Insert one configuration into the database") {
     init {
         context {
@@ -358,6 +387,7 @@ class SandboxCommand : CliktCommand(invokeWithoutSubcommand 
= true, printHelpOnE
 
 fun main(args: Array<String>) {
     SandboxCommand().subcommands(
+        Superuser(),
         Serve(),
         ResetTables(),
         Config(),
@@ -496,6 +526,9 @@ fun serverMain(dbName: String, port: Int) {
                 return@get
             } */
 
+            /*
+              FIXME: not implemented.
+
             post("/register") {
                 // how to read form-POSTed values?
                 val username = "fixme"
@@ -531,6 +564,11 @@ fun serverMain(dbName: String, port: Int) {
                 return@post
             }
 
+             */
+
+            /*
+            FIXME: will likely be replaced by the Single Page Application
+
             get("/jinja-test") {
                 val template = Resources.toString(
                     Resources.getResource("templates/hello.html"),
@@ -542,6 +580,12 @@ fun serverMain(dbName: String, port: Int) {
                 return@get
             }
 
+             */
+
+            /*
+
+            FIXME: not used
+
             authenticate("auth-form") {
                 get("/profile") {
                     val userSession = call.principal<UserIdPrincipal>()
@@ -551,12 +595,18 @@ fun serverMain(dbName: String, port: Int) {
                 }
             }
 
+             */
+
+            /*
+            FIXME: not used
+
             static("/static") {
                 /**
                  * Here Sandbox will serve the CSS files.
                  */
                 resources("static")
             }
+             */
 
             get("/") {
                 call.respondText("Hello, this is Sandbox\n", 
ContentType.Text.Plain)
@@ -574,6 +624,7 @@ fun serverMain(dbName: String, port: Int) {
              * requesting account.
              */
             post("/admin/payments/camt") {
+                requireSuperuser(call.request)
                 val body = call.receiveJson<CamtParams>()
                 val bankaccount = getAccountFromLabel(body.bankaccount)
                 if(body.type != 53) throw SandboxError(
@@ -595,6 +646,7 @@ fun serverMain(dbName: String, port: Int) {
             }
 
             post("/admin/bank-accounts/{label}") {
+                requireSuperuser(call.request)
                 val body = call.receiveJson<BankAccountInfo>()
                 transaction {
                     BankAccountEntity.new {
@@ -610,6 +662,7 @@ fun serverMain(dbName: String, port: Int) {
             }
 
             get("/admin/bank-accounts/{label}") {
+                requireSuperuser(call.request)
                 val label = ensureNonNull(call.parameters["label"])
                 val ret = transaction {
                     val bankAccount = BankAccountEntity.find {
@@ -632,6 +685,7 @@ fun serverMain(dbName: String, port: Int) {
             }
 
             post("/admin/bank-accounts/{label}/simulate-incoming-transaction") 
{
+                requireSuperuser(call.request)
                 val body = call.receiveJson<IncomingPaymentInfo>()
                 // FIXME: generate nicer UUID!
                 val accountLabel = ensureNonNull(call.parameters["label"])
@@ -674,6 +728,7 @@ fun serverMain(dbName: String, port: Int) {
              * Associates a new bank account with an existing Ebics subscriber.
              */
             post("/admin/ebics/bank-accounts") {
+                requireSuperuser(call.request)
                 val body = call.receiveJson<BankAccountRequest>()
                 if (!validateBic(body.bic)) {
                     throw SandboxError(HttpStatusCode.BadRequest, "invalid BIC 
(${body.bic})")
@@ -703,6 +758,7 @@ fun serverMain(dbName: String, port: Int) {
                 return@post
             }
             get("/admin/bank-accounts") {
+                requireSuperuser(call.request)
                 val accounts = mutableListOf<BankAccountInfo>()
                 transaction {
                     BankAccountEntity.all().forEach {
@@ -720,6 +776,7 @@ fun serverMain(dbName: String, port: Int) {
                 call.respond(accounts)
             }
             get("/admin/bank-accounts/{label}/transactions") {
+                requireSuperuser(call.request)
                 val ret = AccountTransactions()
                 transaction {
                     val accountLabel = ensureNonNull(call.parameters["label"])
@@ -758,6 +815,7 @@ fun serverMain(dbName: String, port: Int) {
                 call.respond(ret)
             }
             post("/admin/bank-accounts/{label}/generate-transactions") {
+                requireSuperuser(call.request)
                 transaction {
                     val accountLabel = ensureNonNull(call.parameters["label"])
                     val account = getBankAccountFromLabel(accountLabel)
@@ -809,6 +867,7 @@ fun serverMain(dbName: String, port: Int) {
              * Creates a new Ebics subscriber.
              */
             post("/admin/ebics/subscribers") {
+                requireSuperuser(call.request)
                 val body = call.receiveJson<EbicsSubscriberElement>()
                 transaction {
                     EbicsSubscriberEntity.new {
@@ -830,6 +889,7 @@ fun serverMain(dbName: String, port: Int) {
              * Shows all the Ebics subscribers' details.
              */
             get("/admin/ebics/subscribers") {
+                requireSuperuser(call.request)
                 val ret = AdminGetSubscribers()
                 transaction {
                     EbicsSubscriberEntity.all().forEach {
@@ -846,6 +906,7 @@ fun serverMain(dbName: String, port: Int) {
                 return@get
             }
             post("/admin/ebics/hosts/{hostID}/rotate-keys") {
+                requireSuperuser(call.request)
                 val hostID: String = call.parameters["hostID"] ?: throw 
SandboxError(
                     HttpStatusCode.BadRequest, "host ID missing in URL"
                 )
@@ -874,6 +935,7 @@ fun serverMain(dbName: String, port: Int) {
              * Creates a new EBICS host.
              */
             post("/admin/ebics/hosts") {
+                requireSuperuser(call.request)
                 val req = call.receiveJson<EbicsHostCreateRequest>()
                 val pairA = CryptoUtil.generateRsaKeyPair(2048)
                 val pairB = CryptoUtil.generateRsaKeyPair(2048)
@@ -899,6 +961,7 @@ fun serverMain(dbName: String, port: Int) {
              * Show the names of all the Ebics hosts
              */
             get("/admin/ebics/hosts") {
+                requireSuperuser(call.request)
                 val ebicsHosts = transaction {
                     EbicsHostEntity.all().map { it.hostId }
                 }
diff --git a/util/src/main/kotlin/CryptoUtil.kt 
b/util/src/main/kotlin/CryptoUtil.kt
index b92b626..6c5f99f 100644
--- a/util/src/main/kotlin/CryptoUtil.kt
+++ b/util/src/main/kotlin/CryptoUtil.kt
@@ -19,6 +19,8 @@
 
 package tech.libeufin.util
 
+import UtilError
+import io.ktor.http.*
 import net.taler.wallet.crypto.Base32Crockford
 import org.bouncycastle.jce.provider.BouncyCastleProvider
 import java.io.ByteArrayOutputStream
@@ -308,6 +310,18 @@ object CryptoUtil {
         return "sha256-salted\$$salt\$$pwh"
     }
 
+    /**
+     * Throws error when credentials don't match.  Only returns in case of 
success.
+     */
+    fun checkPwOrThrow(pw: String, storedPwHash: String): Boolean {
+        if(!this.checkpw(pw, storedPwHash)) throw UtilError(
+            HttpStatusCode.Forbidden,
+            "Credentials did not match",
+            LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
+        )
+        return true
+    }
+
     fun checkpw(pw: String, storedPwHash: String): Boolean {
         val components = storedPwHash.split('$')
         if (components.size < 2) {
diff --git a/util/src/main/kotlin/EbicsOrderUtil.kt 
b/util/src/main/kotlin/EbicsOrderUtil.kt
index bcdc836..64f1fd3 100644
--- a/util/src/main/kotlin/EbicsOrderUtil.kt
+++ b/util/src/main/kotlin/EbicsOrderUtil.kt
@@ -55,7 +55,7 @@ object EbicsOrderUtil {
         val rng = SecureRandom()
         val res = ByteArray(16)
         rng.nextBytes(res)
-        return res.toHexString().toUpperCase()
+        return res.toHexString().uppercase()
     }
 
     /**
diff --git a/util/src/main/kotlin/Errors.kt b/util/src/main/kotlin/Errors.kt
index 73ad892..55ad871 100644
--- a/util/src/main/kotlin/Errors.kt
+++ b/util/src/main/kotlin/Errors.kt
@@ -1,6 +1,9 @@
 import io.ktor.http.*
+import tech.libeufin.util.LibeufinErrorCode
 import tech.libeufin.util.TalerErrorCode
 import kotlin.system.exitProcess
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
 
 /*
  * This file is part of LibEuFin.
@@ -21,10 +24,12 @@ import kotlin.system.exitProcess
  * <http://www.gnu.org/licenses/>
  */
 
+val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util")
+
 data class UtilError(
     val statusCode: HttpStatusCode,
     val reason: String,
-    val ec: TalerErrorCode?
+    val ec: LibeufinErrorCode?
 ) :
     Exception("$reason (HTTP status $statusCode)")
 
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
new file mode 100644
index 0000000..69ed7a7
--- /dev/null
+++ b/util/src/main/kotlin/HTTP.kt
@@ -0,0 +1,43 @@
+package tech.libeufin.util
+
+import UtilError
+import io.ktor.http.*
+import io.ktor.request.*
+import logger
+
+fun getHTTPBasicAuthCredentials(request: ApplicationRequest): Pair<String, 
String> {
+    val authHeader = getAuthorizationHeader(request)
+    return extractUserAndPassword(authHeader)
+}
+
+/**
+ * Extracts the Authorization:-header line and throws error if not found.
+ */
+fun getAuthorizationHeader(request: ApplicationRequest): String {
+    val authorization = request.headers["Authorization"]
+    return authorization ?: throw UtilError(
+        HttpStatusCode.BadRequest, "Authorization header not found",
+        LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
+    )
+}
+
+/**
+ * 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 UtilError(
+            HttpStatusCode.BadRequest,
+            "invalid Authorization:-header received",
+            LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
+        )
+    }
+    return Pair(username, password)
+}
diff --git a/util/src/main/kotlin/LibeufinErrorCodes.kt 
b/util/src/main/kotlin/LibeufinErrorCodes.kt
index f1c1828..3bc5fbc 100644
--- a/util/src/main/kotlin/LibeufinErrorCodes.kt
+++ b/util/src/main/kotlin/LibeufinErrorCodes.kt
@@ -48,5 +48,17 @@ enum class LibeufinErrorCode(val code: Int) {
      * A bank's invariant is not holding anymore.  For example, a customer's
      * balance doesn't match the history of their bank account.
      */
-    LIBEUFIN_EC_INCONSISTENT_STATE(3)
+    LIBEUFIN_EC_INCONSISTENT_STATE(3),
+
+    /**
+     * An access was forbidden due to wrong credentials.
+     */
+    LIBEUFIN_EC_AUTHENTICATION_FAILED(4),
+
+    /**
+     * A parameter in the request was malformed.
+     * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+     * (A value of 0 indicates that the error is generated client-side).
+     */
+    LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED(5),
 }
\ No newline at end of file
diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt
index fcd12f9..5e0780b 100644
--- a/util/src/main/kotlin/XMLUtil.kt
+++ b/util/src/main/kotlin/XMLUtil.kt
@@ -61,8 +61,7 @@ import javax.xml.validation.Validator
 import javax.xml.xpath.XPath
 import javax.xml.xpath.XPathConstants
 import javax.xml.xpath.XPathFactory
-
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util")
+import logger
 
 class DefaultNamespaces : NamespacePrefixMapper() {
     override fun getPreferredPrefix(namespaceUri: String?, suggestion: 
String?, requirePrefix: Boolean): String? {
diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt
index 7fd464c..e3f6565 100644
--- a/util/src/main/kotlin/strings.kt
+++ b/util/src/main/kotlin/strings.kt
@@ -96,7 +96,7 @@ fun chunkString(input: String): String {
         }
         ret.append(input[i])
     }
-    return ret.toString().toUpperCase()
+    return ret.toString().uppercase()
 }
 
 data class AmountWithCurrency(
@@ -109,7 +109,7 @@ fun parseDecimal(decimalStr: String): BigDecimal {
         throw UtilError(
             HttpStatusCode.BadRequest,
             "Bad string amount given: $decimalStr",
-            TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MALFORMED
+            LibeufinErrorCode.LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED
         )
     return try {
         BigDecimal(decimalStr)
@@ -117,7 +117,7 @@ fun parseDecimal(decimalStr: String): BigDecimal {
         throw UtilError(
             HttpStatusCode.BadRequest,
             "Bad string amount given: $decimalStr",
-            TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MALFORMED
+            LibeufinErrorCode.LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED
         )
     }
 }

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]