summaryrefslogtreecommitdiff
path: root/nexus
diff options
context:
space:
mode:
authorAntoine A <>2024-04-29 17:25:43 +0900
committerAntoine A <>2024-04-29 17:25:43 +0900
commite8490074b3d174acb7863a36c3529792c83158e9 (patch)
tree8a9cc51b9f3289454a9e63b30b1114bf71f7d63c /nexus
parent6aaf661c073d5ffd74f4407fd386f7df4d0702de (diff)
downloadlibeufin-e8490074b3d174acb7863a36c3529792c83158e9.tar.gz
libeufin-e8490074b3d174acb7863a36c3529792c83158e9.tar.bz2
libeufin-e8490074b3d174acb7863a36c3529792c83158e9.zip
nexus: wire gateway auth
Diffstat (limited to 'nexus')
-rw-r--r--nexus/conf/mini.conf15
-rw-r--r--nexus/conf/test.conf7
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt32
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt2
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt65
-rw-r--r--nexus/src/test/kotlin/WireGatewayApiTest.kt45
-rw-r--r--nexus/src/test/kotlin/helpers.kt43
-rw-r--r--nexus/src/test/kotlin/routines.kt146
8 files changed, 219 insertions, 136 deletions
diff --git a/nexus/conf/mini.conf b/nexus/conf/mini.conf
new file mode 100644
index 00000000..1b52e17f
--- /dev/null
+++ b/nexus/conf/mini.conf
@@ -0,0 +1,15 @@
+[nexus-ebics]
+CURRENCY = CHF
+BANK_DIALECT = postfinance
+HOST_BASE_URL = https://isotest.postfinance.ch/ebicsweb/ebicsweb
+BANK_PUBLIC_KEYS_FILE = test/tmp/bank-keys.json
+CLIENT_PRIVATE_KEYS_FILE = test/tmp/client-keys.json
+IBAN = CH7789144474425692816
+HOST_ID = PFEBICS
+USER_ID = PFC00563
+PARTNER_ID = PFC00563
+BIC = BIC
+NAME = myname
+
+[libeufin-nexusdb-postgres]
+CONFIG = postgres:///libeufincheck \ No newline at end of file
diff --git a/nexus/conf/test.conf b/nexus/conf/test.conf
index e6d52fff..dd7f3196 100644
--- a/nexus/conf/test.conf
+++ b/nexus/conf/test.conf
@@ -15,4 +15,9 @@ NAME = myname
CONFIG = postgres:///libeufincheck
[nexus-fetch]
-IGNORE_TRANSACTIONS_BEFORE = 2024-04-04 \ No newline at end of file
+IGNORE_TRANSACTIONS_BEFORE = 2024-04-04
+
+[nexus-httpd-wire-gateway-api]
+ENABLED = YES
+AUTH_METHOD = token
+AUTH_TOKEN = secret-token \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt
index eb1bac91..95cc17af 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Config.kt
@@ -31,6 +31,10 @@ class NexusFetchConfig(config: TalerConfig) {
val ignoreBefore = config.lookupDate("nexus-fetch", "ignore_transactions_before")
}
+class ApiConfig(config: TalerConfig, section: String) {
+ val authMethod = config.requireAuthMethod(section)
+}
+
/** Configuration for libeufin-nexus */
class NexusConfig(val config: TalerConfig) {
private fun requireString(option: String): String = config.requireString("nexus-ebics", option)
@@ -65,6 +69,9 @@ class NexusConfig(val config: TalerConfig) {
"gls" -> Dialect.gls
else -> throw TalerConfigError.invalid("dialct", "libeufin-nexus", "bank_dialect", "expected 'postfinance' or 'gls' got '$type'")
}
+
+ val wireGatewayApiCfg = config.apiConf("nexus-httpd-wire-gateway-api")
+ val revenueApiCfg = config.apiConf("nexus-httpd-revenue-api")
}
fun NexusConfig.checkCurrency(amount: TalerAmount) {
@@ -72,4 +79,29 @@ fun NexusConfig.checkCurrency(amount: TalerAmount) {
"Wrong currency: expected regional $currency got ${amount.currency}",
TalerErrorCode.GENERIC_CURRENCY_MISMATCH
)
+}
+
+fun TalerConfig.requireAuthMethod(section: String): AuthMethod {
+ return when (val method = requireString(section, "auth_method", "auth method")) {
+ "none" -> AuthMethod.None
+ "token" -> {
+ val token = requireString(section, "auth_token")
+ AuthMethod.Basic(token)
+ }
+ else -> throw TalerConfigError.invalid("auth method target type", section, "auth_method", "expected 'token' or 'none' got '$method'")
+ }
+}
+
+fun TalerConfig.apiConf(section: String): ApiConfig? {
+ val enabled = requireBoolean(section, "enabled")
+ return if (enabled) {
+ return ApiConfig(this, section)
+ } else {
+ null
+ }
+}
+
+sealed interface AuthMethod {
+ data object None: AuthMethod
+ data class Basic(val token: String): AuthMethod
} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
index 6073072b..185f3b0c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
@@ -34,7 +34,7 @@ import tech.libeufin.nexus.db.ExchangeDAO.*
import java.time.Instant
-fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) {
+fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) = authApi(cfg.wireGatewayApiCfg) {
get("/taler-wire-gateway/config") {
call.respond(WireGatewayConfig(
currency = cfg.currency
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt
new file mode 100644
index 00000000..f7de32eb
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt
@@ -0,0 +1,65 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.nexus.api
+
+import tech.libeufin.nexus.*
+import tech.libeufin.common.*
+import tech.libeufin.common.api.*
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
+import io.ktor.util.*
+import io.ktor.util.pipeline.*
+
+/** Apply api configuration for a route: conditional access and authentication */
+fun Route.authApi(cfg: ApiConfig?, callback: Route.() -> Unit): Route =
+ intercept(callback) {
+ if (cfg == null) {
+ throw apiError(HttpStatusCode.NotImplemented, "API not implemented", TalerErrorCode.END)
+ }
+ val header = context.request.headers["Authorization"]
+ // Basic auth challenge
+ when (cfg.authMethod) {
+ AuthMethod.None -> {}
+ is AuthMethod.Basic -> {
+ if (header == null) {
+ //response.header(HttpHeaders.WWWAuthenticate, "Basic") ?
+ throw unauthorized(
+ "Authorization header not found",
+ TalerErrorCode.GENERIC_PARAMETER_MISSING
+ )
+ }
+ val (scheme, content) = header.splitOnce(" ") ?: throw badRequest(
+ "Authorization is invalid",
+ TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ when (scheme) {
+ "Basic", "Bearer" -> {
+ // TODO choose between one of those
+ if (content != cfg.authMethod.token) {
+ throw unauthorized("Unknown token")
+ }
+ }
+ else -> throw unauthorized("Authorization method wrong or not supported")
+ }
+ }
+ }
+ } \ No newline at end of file
diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt b/nexus/src/test/kotlin/WireGatewayApiTest.kt
index 2b662e83..224cd513 100644
--- a/nexus/src/test/kotlin/WireGatewayApiTest.kt
+++ b/nexus/src/test/kotlin/WireGatewayApiTest.kt
@@ -30,9 +30,9 @@ class WireGatewayApiTest {
// GET /accounts/{USERNAME}/taler-wire-gateway/config
@Test
fun config() = serverSetup { _ ->
- //authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/config")
+ authRoutine(HttpMethod.Get, "/taler-wire-gateway/config")
- client.get("/taler-wire-gateway/config").assertOk()
+ client.getA("/taler-wire-gateway/config").assertOk()
}
// Testing the POST /transfer call from the TWG API.
@@ -46,20 +46,20 @@ class WireGatewayApiTest {
"credit_account" to grothoffPayto
}
- //authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/transfer", valid_req)
+ authRoutine(HttpMethod.Post, "/taler-wire-gateway/transfer")
// Check OK
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req)
}.assertOk()
// check idempotency
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req)
}.assertOk()
// Trigger conflict due to reused request_uid
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req) {
"wtid" to ShortHashCode.rand()
"exchange_base_url" to "http://different-exchange.example.com/"
@@ -67,35 +67,35 @@ class WireGatewayApiTest {
}.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
// Currency mismatch
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req) {
"amount" to "EUR:33"
}
}.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
// Bad BASE32 wtid
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req) {
"wtid" to "I love chocolate"
}
}.assertBadRequest()
// Bad BASE32 len wtid
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req) {
"wtid" to Base32Crockford.encode(ByteArray(31).rand())
}
}.assertBadRequest()
// Bad BASE32 request_uid
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req) {
"request_uid" to "I love chocolate"
}
}.assertBadRequest()
// Bad BASE32 len wtid
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json(valid_req) {
"request_uid" to Base32Crockford.encode(ByteArray(65).rand())
}
@@ -107,13 +107,13 @@ class WireGatewayApiTest {
*/
@Test
fun historyIncoming() = serverSetup { db ->
- //authRoutine(HttpMethod.Get, "/taler-wire-gateway/history/incoming")
+ authRoutine(HttpMethod.Get, "/taler-wire-gateway/history/incoming")
historyRoutine<IncomingHistory>(
url = "/taler-wire-gateway/history/incoming",
ids = { it.incoming_transactions.map { it.row_id } },
registered = listOf(
{
- client.post("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
json {
"amount" to "CHF:12"
"reserve_pub" to EddsaPublicKey.rand()
@@ -144,7 +144,7 @@ class WireGatewayApiTest {
*/
@Test
fun historyOutgoing() = serverSetup { db ->
- //authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/outgoing")
+ authRoutine(HttpMethod.Get, "/taler-wire-gateway/history/outgoing")
historyRoutine<OutgoingHistory>(
url = "/taler-wire-gateway/history/outgoing",
ids = { it.outgoing_transactions.map { it.row_id } },
@@ -182,35 +182,40 @@ class WireGatewayApiTest {
"debit_account" to grothoffPayto
}
- //authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/admin/add-incoming", valid_req, requireAdmin = true)
+ authRoutine(HttpMethod.Post, "/taler-wire-gateway/admin/add-incoming")
// Check OK
- client.post("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
json(valid_req)
}.assertOk()
// Trigger conflict due to reused reserve_pub
- client.post("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
json(valid_req)
}.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
// Currency mismatch
- client.post("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
json(valid_req) { "amount" to "EUR:33" }
}.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
// Bad BASE32 reserve_pub
- client.post("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
json(valid_req) {
"reserve_pub" to "I love chocolate"
}
}.assertBadRequest()
// Bad BASE32 len reserve_pub
- client.post("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
json(valid_req) {
"reserve_pub" to Base32Crockford.encode(ByteArray(31).rand())
}
}.assertBadRequest()
}
+
+ @Test
+ fun noApi() = serverSetup("mini.conf") { _ ->
+ client.get("/taler-wire-gateway/config").assertNotImplemented()
+ }
} \ No newline at end of file
diff --git a/nexus/src/test/kotlin/helpers.kt b/nexus/src/test/kotlin/helpers.kt
index dc5a98a8..2303a1ea 100644
--- a/nexus/src/test/kotlin/helpers.kt
+++ b/nexus/src/test/kotlin/helpers.kt
@@ -57,7 +57,7 @@ fun setup(
fun serverSetup(
conf: String = "test.conf",
lambda: suspend ApplicationTestBuilder.(Database) -> Unit
-) = setup { db, cfg ->
+) = setup(conf) { db, cfg ->
testApplication {
application {
nexusApi(db, cfg)
@@ -129,7 +129,7 @@ fun genOutPay(subject: String, messageId: String? = null): OutgoingPayment {
/** Perform a taler outgoing transaction */
suspend fun ApplicationTestBuilder.transfer() {
- client.post("/taler-wire-gateway/transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
json {
"request_uid" to HashCode.rand()
"amount" to "CHF:55"
@@ -150,4 +150,43 @@ suspend fun talerableOut(db: Database) {
suspend fun talerableIn(db: Database) {
val reserve_pub = ShortHashCode.rand()
ingestIncomingPayment(db, genInPay("history test with $reserve_pub reserve pub"))
+}
+
+
+/* ----- Auth ----- */
+
+/** Auto auth get request */
+suspend inline fun HttpClient.getA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
+ return get(url) {
+ auth()
+ builder(this)
+ }
+}
+
+/** Auto auth post request */
+suspend inline fun HttpClient.postA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
+ return post(url) {
+ auth()
+ builder(this)
+ }
+}
+
+/** Auto auth patch request */
+suspend inline fun HttpClient.patchA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
+ return patch(url) {
+ auth()
+ builder(this)
+ }
+}
+
+/** Auto auth delete request */
+suspend inline fun HttpClient.deleteA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
+ return delete(url) {
+ auth()
+ builder(this)
+ }
+}
+
+fun HttpRequestBuilder.auth() {
+ headers["Authorization"] = "Bearer secret-token"
} \ No newline at end of file
diff --git a/nexus/src/test/kotlin/routines.kt b/nexus/src/test/kotlin/routines.kt
index 9503565e..7b92dea7 100644
--- a/nexus/src/test/kotlin/routines.kt
+++ b/nexus/src/test/kotlin/routines.kt
@@ -29,123 +29,45 @@ import tech.libeufin.common.*
import tech.libeufin.common.test.*
import kotlin.test.assertEquals
-suspend inline fun <reified B> ApplicationTestBuilder.historyRoutine(
- url: String,
- crossinline ids: (B) -> List<Long>,
- registered: List<suspend () -> Unit>,
- ignored: List<suspend () -> Unit> = listOf(),
- polling: Boolean = true,
- auth: String? = null
-) {
- // Get history
- val history: suspend (String) -> HttpResponse = { params: String ->
- client.get("$url?$params") {
- //pwAuth(auth)
- }
- }
- // Check history is following specs
- val assertHistory: suspend HttpResponse.(Int) -> Unit = { size: Int ->
- assertHistoryIds<B>(size, ids)
- }
- // Get latest registered id
- val latestId: suspend () -> Long = {
- history("delta=-1").assertOkJson<B>().run { ids(this)[0] }
- }
- // Check error when no transactions
- history("delta=7").assertNoContent()
-
- // Run interleaved registered and ignore transactions
- val registered_iter = registered.iterator()
- val ignored_iter = ignored.iterator()
- while (registered_iter.hasNext() || ignored_iter.hasNext()) {
- if (registered_iter.hasNext()) registered_iter.next()()
- if (ignored_iter.hasNext()) ignored_iter.next()()
- }
+// Test endpoint is correctly authenticated
+suspend fun ApplicationTestBuilder.authRoutine(
+ method: HttpMethod,
+ path: String
+) {
+ // No header
+ client.request(path) {
+ this.method = method
+ }.assertUnauthorized(TalerErrorCode.GENERIC_PARAMETER_MISSING)
+ // Bad header
+ client.request(path) {
+ this.method = method
+ headers["Authorization"] = "WTF"
+ }.assertBadRequest(TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED)
- val nbRegistered = registered.size
- val nbIgnored = ignored.size
- val nbTotal = nbRegistered + nbIgnored
+ // Bad token
+ client.request(path) {
+ this.method = method
+ headers["Authorization"] = "Bearer bad-token"
+ }.assertUnauthorized()
- // Check ignored
- history("delta=$nbTotal").assertHistory(nbRegistered)
- // Check skip ignored
- history("delta=$nbRegistered").assertHistory(nbRegistered)
+ // GLS deployment
+ // - testing did work ?
+ // token - basic bearer
+ // libeufin-nexus
+ // - wire gateway try camt.052 files
+}
- if (polling) {
- // Check no polling when we cannot have more transactions
- assertTime(0, 100) {
- history("delta=-${nbRegistered+1}&long_poll_ms=1000")
- .assertHistory(nbRegistered)
- }
- // Check no polling when already find transactions even if less than delta
- assertTime(0, 100) {
- history("delta=${nbRegistered+1}&long_poll_ms=1000")
- .assertHistory(nbRegistered)
- }
- // Check polling
- coroutineScope {
- val id = latestId()
- launch { // Check polling succeed
- assertTime(100, 200) {
- history("delta=2&start=$id&long_poll_ms=1000")
- .assertHistory(1)
- }
- }
- launch { // Check polling timeout
- assertTime(200, 300) {
- history("delta=1&start=${id+nbTotal*3}&long_poll_ms=200")
- .assertNoContent()
- }
- }
- delay(100)
- registered[0]()
- }
-
- // Test triggers
- for (register in registered) {
- coroutineScope {
- val id = latestId()
- launch {
- assertTime(100, 200) {
- history("delta=7&start=$id&long_poll_ms=1000")
- .assertHistory(1)
- }
- }
- delay(100)
- register()
- }
- }
-
- // Test doesn't trigger
- coroutineScope {
- val id = latestId()
- launch {
- assertTime(200, 300) {
- history("delta=7&start=$id&long_poll_ms=200")
- .assertNoContent()
- }
- }
- delay(100)
- for (ignore in ignored) {
- ignore()
- }
- }
- }
-
- // Testing ranges.
- repeat(20) {
- registered[0]()
+suspend inline fun <reified B> ApplicationTestBuilder.historyRoutine(
+ url: String,
+ crossinline ids: (B) -> List<Long>,
+ registered: List<suspend () -> Unit>,
+ ignored: List<suspend () -> Unit> = listOf(),
+ polling: Boolean = true
+) {
+ abstractHistoryRoutine(ids, registered, ignored, polling) { params: String ->
+ client.getA("$url?$params")
}
- val id = latestId()
- // Default
- history("").assertHistory(20)
- // forward range:
- history("delta=10").assertHistory(10)
- history("delta=10&start=4").assertHistory(10)
- // backward range:
- history("delta=-10").assertHistory(10)
- history("delta=-10&start=${id-4}").assertHistory(10)
} \ No newline at end of file