libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 09aed8c3210acb636cd09b8babe55dc3a99e7b42
parent 4995252f12d42705718e08535cf88c78ed0c615c
Author: MS <ms@taler.net>
Date:   Thu,  9 Feb 2023 17:34:02 +0100

circuit API

allowing a filter over the cash-out author

Diffstat:
Mnexus/src/test/kotlin/MakeEnv.kt | 1+
Mnexus/src/test/kotlin/SandboxCircuitApiTest.kt | 32+++++++++++++++++++++++++++++++-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt | 51+++++++++++++++++++++++++++++++++++++++++++--------
3 files changed, 75 insertions(+), 9 deletions(-)

diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt @@ -246,6 +246,7 @@ fun prepSandboxDb() { username = "bar" passwordHash = CryptoUtil.hashpw("bar") name = "Bar" + cashout_address = "payto://iban/FIAT" } DemobankCustomerEntity.new { username = "baz" diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt @@ -26,6 +26,24 @@ class SandboxCircuitApiTest { } } + @Test + fun accessAccountsTest() { + withTestDatabase { + prepSandboxDb() + testApplication { + application(sandboxApp) + val R = client.get("/demobanks/default/circuit-api/accounts/bar") { + basicAuth("foo", "foo") + expectSuccess = false + } + assert(R.status.value == HttpStatusCode.Forbidden.value) + client.get("/demobanks/default/circuit-api/accounts/bar") { + basicAuth("admin", "foo") + expectSuccess = true + } + } + } + } // Only tests that the calls get a 2xx status code. @Test fun listAccountsTest() { @@ -96,7 +114,7 @@ class SandboxCircuitApiTest { subject = "unused" creationTime = 0L tanChannel = SupportedTanChannels.FILE // change type to enum? - account = "unused" + account = "foo" status = CashoutOperationStatus.PENDING } } @@ -118,6 +136,18 @@ class SandboxCircuitApiTest { val status = respJson.get("status").asText() assert(status.uppercase() == "PENDING") println(R.bodyAsText()) + // Check that bar doesn't get foo's cash-out + R = client.get("/demobanks/default/circuit-api/cashouts?account=foo") { + expectSuccess = false + basicAuth("bar", "bar") + } + assert(R.status.value == HttpStatusCode.Forbidden.value) + // Check that foo can get its own + R = client.get("/demobanks/default/circuit-api/cashouts?account=foo") { + expectSuccess = false + basicAuth("foo", "foo") + } + assert(R.status.value == HttpStatusCode.OK.value) } } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -290,14 +290,43 @@ fun circuitApi(circuitRoute: Route) { call.respond(ret) return@get } - // Gets the list of all the cash-out operations. + // Gets the list of all the cash-out operations, + // or those belonging to the account given as a parameter. circuitRoute.get("/cashouts") { - call.request.basicAuth(onlyAdmin = true) + val user = call.request.basicAuth() + val whichAccount = call.request.queryParameters["account"] + /** + * Only admin's allowed to omit the target account (= get + * all the accounts) or to check other customers cash-out + * operations. + */ + if (user != "admin" && whichAccount != user) throw forbidden( + "Ordinary users can only request their own account" + ) + /** + * At this point, the client has the rights over the account(s) + * whose operations are to be returned. Double-checking that + * Admin doesn't ask its own cash-outs, since that's not supported. + */ + if (whichAccount == "admin") throw badRequest("Cash-out for admin is not supported") + + // Preparing the response. val node = jacksonObjectMapper().createObjectNode() val maybeArray = node.putArray("cashouts") - transaction { - CashoutOperationEntity.all().forEach { - maybeArray.add(it.uuid.toString()) + + if (whichAccount == null) { // no target account, return all the cash-outs + transaction { + CashoutOperationEntity.all().forEach { + maybeArray.add(it.uuid.toString()) + } + } + } else { // do filter on the target account. + transaction { + CashoutOperationEntity.find { + CashoutOperationsTable.account eq whichAccount + }.forEach { + maybeArray.add(it.uuid.toString()) + } } } if (maybeArray.size() == 0) { @@ -445,7 +474,9 @@ fun circuitApi(circuitRoute: Route) { val username = call.request.basicAuth() val resourceName = call.getUriComponent("resourceName") throwIfInstitutionalName(resourceName) - allowOwnerOrAdmin(username, resourceName) + if (!allowOwnerOrAdmin(username, resourceName)) throw forbidden( + "User $username has no rights over $resourceName" + ) val customer = getCustomer(resourceName) /** * CUSTOMER AND BANK ACCOUNT INVARIANT. @@ -506,7 +537,9 @@ fun circuitApi(circuitRoute: Route) { val username = call.request.basicAuth() val customerUsername = call.getUriComponent("customerUsername") throwIfInstitutionalName(customerUsername) - allowOwnerOrAdmin(username, customerUsername) + if (!allowOwnerOrAdmin(username, customerUsername)) throw forbidden( + "User $username has no rights over $customerUsername" + ) // Flow here means admin or username have the rights for this operation. val req = call.receive<AccountPasswordChange>() /** @@ -528,7 +561,9 @@ fun circuitApi(circuitRoute: Route) { throw internalServerError("Authentication disabled, don't have a default for this request.") val resourceName = call.getUriComponent("resourceName") throwIfInstitutionalName(resourceName) - allowOwnerOrAdmin(username, resourceName) + if(!allowOwnerOrAdmin(username, resourceName)) throw forbidden( + "User $username has no rights over $resourceName" + ) // account found and authentication succeeded val req = call.receive<CircuitAccountReconfiguration>() // Only admin's allowed to change the legal name