commit e8490074b3d174acb7863a36c3529792c83158e9 parent 6aaf661c073d5ffd74f4407fd386f7df4d0702de Author: Antoine A <> Date: Mon, 29 Apr 2024 17:25:43 +0900 nexus: wire gateway auth Diffstat:
17 files changed, 433 insertions(+), 282 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt @@ -28,6 +28,7 @@ import io.ktor.util.pipeline.* import tech.libeufin.bank.* import tech.libeufin.bank.db.Database import tech.libeufin.common.* +import tech.libeufin.common.api.* import tech.libeufin.common.crypto.PwCrypto import java.time.Instant diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -37,6 +37,7 @@ import tech.libeufin.bank.auth.username import tech.libeufin.bank.db.AccountDAO.AccountCreationResult import tech.libeufin.bank.db.Database import tech.libeufin.common.* +import tech.libeufin.common.api.* import java.util.* fun ApplicationCall.uuidPath(name: String): UUID { @@ -132,20 +133,6 @@ suspend fun createAdminAccount(db: Database, cfg: BankConfig, pw: String? = null ) } -fun Route.intercept(callback: Route.() -> Unit, interceptor: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit): Route { - val subRoute = createChild(object : RouteSelector() { - override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation = - RouteSelectorEvaluation.Constant - }) - subRoute.intercept(ApplicationCallPipeline.Plugins) { - interceptor() - proceed() - } - - callback(subRoute) - return subRoute -} - fun Route.conditional(implemented: Boolean, callback: Route.() -> Unit): Route = intercept(callback) { if (!implemented) { diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt @@ -23,10 +23,8 @@ import tech.libeufin.bank.MonitorParams import tech.libeufin.bank.MonitorResponse import tech.libeufin.bank.MonitorWithConversion import tech.libeufin.bank.Timeframe -import tech.libeufin.common.ShortHashCode -import tech.libeufin.common.TalerAmount import tech.libeufin.common.db.executeQueryCheck -import tech.libeufin.common.micros +import tech.libeufin.common.* import java.time.Instant import java.time.LocalDateTime import java.time.ZoneOffset diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -352,22 +352,6 @@ fun assertException(msg: String, lambda: () -> Unit) { } } -/* ----- Body helper ----- */ - -suspend inline fun <reified B> HttpResponse.assertOkJson(lambda: (B) -> Unit = {}): B { - assertOk() - val body = json<B>() - lambda(body) - return body -} - -suspend inline fun <reified B> HttpResponse.assertAcceptedJson(lambda: (B) -> Unit = {}): B { - assertAccepted() - val body = json<B>() - lambda(body) - return body -} - /* ----- Auth ----- */ /** Auto auth get request */ @@ -412,7 +396,6 @@ fun HttpRequestBuilder.pwAuth(username: String? = null) { val login = url.pathSegments[2] basicAuth("$login", "$login-password") } - } /* ----- Random data generation ----- */ diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt @@ -41,6 +41,17 @@ suspend fun ApplicationTestBuilder.authRoutine( allowAdmin: Boolean = false ) { // No body when authentication must happen before parsing the body + + // 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) // Unknown account client.request(path) { @@ -61,7 +72,7 @@ suspend fun ApplicationTestBuilder.authRoutine( }.assertUnauthorized() if (requireAdmin) { - // Not exchange account + // Not exchange account client.request(path) { this.method = method pwAuth("merchant") @@ -92,117 +103,11 @@ suspend inline fun <reified B> ApplicationTestBuilder.historyRoutine( polling: Boolean = true, auth: String? = null ) { - // Get history - val history: suspend (String) -> HttpResponse = { params: String -> + abstractHistoryRoutine(ids, registered, ignored, polling) { 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()() - } - - - val nbRegistered = registered.size - val nbIgnored = ignored.size - val nbTotal = nbRegistered + nbIgnored - - // Check ignored - history("delta=$nbTotal").assertHistory(nbRegistered) - // Check skip ignored - history("delta=$nbRegistered").assertHistory(nbRegistered) - - 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]() - } - 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) } suspend inline fun <reified B> ApplicationTestBuilder.statusRoutine( diff --git a/common/src/main/kotlin/Client.kt b/common/src/main/kotlin/Client.kt @@ -63,7 +63,14 @@ suspend inline fun <reified B> HttpResponse.json(): B = Json.decodeFromString(kotlinx.serialization.serializer<B>(), bodyAsText()) suspend inline fun <reified B> HttpResponse.assertOkJson(lambda: (B) -> Unit = {}): B { - assertEquals(HttpStatusCode.OK, status) + assertOk() + val body = json<B>() + lambda(body) + return body +} + +suspend inline fun <reified B> HttpResponse.assertAcceptedJson(lambda: (B) -> Unit = {}): B { + assertAccepted() val body = json<B>() lambda(body) return body diff --git a/common/src/main/kotlin/TalerConfig.kt b/common/src/main/kotlin/TalerConfig.kt @@ -444,8 +444,8 @@ class TalerConfig internal constructor( return str } - fun requireString(section: String, option: String): String = - lookupString(section, option) ?: throw TalerConfigError.missing("string", section, option) + fun requireString(section: String, option: String, type: String = "string"): String = + lookupString(section, option) ?: throw TalerConfigError.missing(type, section, option) fun requireNumber(section: String, option: String): Int { val raw = lookupString(section, option) ?: throw TalerConfigError.missing("number", section, option) diff --git a/common/src/main/kotlin/api/route.kt b/common/src/main/kotlin/api/route.kt @@ -0,0 +1,41 @@ +/* + * 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.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.* + +fun Route.intercept(callback: Route.() -> Unit, interceptor: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit): Route { + val subRoute = createChild(object : RouteSelector() { + override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation = + RouteSelectorEvaluation.Constant + }) + subRoute.intercept(ApplicationCallPipeline.Plugins) { + interceptor() + proceed() + } + + callback(subRoute) + return subRoute +} +\ No newline at end of file diff --git a/common/src/main/kotlin/test/routines.kt b/common/src/main/kotlin/test/routines.kt @@ -0,0 +1,142 @@ +/* + * 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.common.test + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.testing.* +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import tech.libeufin.common.* + +suspend inline fun <reified B> ApplicationTestBuilder.abstractHistoryRoutine( + crossinline ids: (B) -> List<Long>, + registered: List<suspend () -> Unit>, + ignored: List<suspend () -> Unit> = listOf(), + polling: Boolean = true, + crossinline history: suspend (String) -> HttpResponse, +) { + // 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()() + } + + val nbRegistered = registered.size + val nbIgnored = ignored.size + val nbTotal = nbRegistered + nbIgnored + + // Check ignored + history("delta=$nbTotal").assertHistory(nbRegistered) + // Check skip ignored + history("delta=$nbRegistered").assertHistory(nbRegistered) + + 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]() + } + 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 diff --git a/nexus/conf/mini.conf 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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