diff --git a/src/main/scala/code/api/util/ApiRole.scala b/src/main/scala/code/api/util/ApiRole.scala index 8f7b25cbe..bbbe4bd42 100644 --- a/src/main/scala/code/api/util/ApiRole.scala +++ b/src/main/scala/code/api/util/ApiRole.scala @@ -12,6 +12,7 @@ object ApiRole { case object CanCreateCustomer extends ApiRole case object CanCreateAccount extends ApiRole case object IsHackathonDeveloper extends ApiRole + case object CanCreateAnyTransactionRequest extends ApiRole def valueOf(value: String): ApiRole = value match { case "CanSearchAllTransactions" => CanSearchAllTransactions @@ -22,6 +23,7 @@ object ApiRole { case "CanCreateCustomer" => CanCreateCustomer case "CanCreateAccount" => CanCreateAccount case "IsHackathonDeveloper" => IsHackathonDeveloper + case "CanCreateAnyTransactionRequest" => CanCreateAnyTransactionRequest case _ => throw new IllegalArgumentException() } diff --git a/src/main/scala/code/api/v2_0_0/APIMethods200.scala b/src/main/scala/code/api/v2_0_0/APIMethods200.scala index dbe1a791b..e63c367bf 100644 --- a/src/main/scala/code/api/v2_0_0/APIMethods200.scala +++ b/src/main/scala/code/api/v2_0_0/APIMethods200.scala @@ -1145,6 +1145,7 @@ trait APIMethods200 { transBody <- tryo{getTransactionRequestBodyFromJson(transBodyJson)} fromBank <- tryo(Bank(bankId).get) ?~! {ErrorMessages.BankNotFound} fromAccount <- tryo(BankAccount(bankId, accountId).get) ?~! {ErrorMessages.AccountNotFound} + isOwnerOrHasEntitlement <- booleanToBox(u.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, u.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) toBankId <- tryo(BankId(transBodyJson.to.bank_id)) toAccountId <- tryo(AccountId(transBodyJson.to.account_id)) toAccount <- tryo{BankAccount(toBankId, toAccountId).get} ?~! {ErrorMessages.CounterpartyNotFound} diff --git a/src/main/scala/code/bankconnectors/Connector.scala b/src/main/scala/code/bankconnectors/Connector.scala index f2c89d914..4d1445347 100644 --- a/src/main/scala/code/bankconnectors/Connector.scala +++ b/src/main/scala/code/bankconnectors/Connector.scala @@ -1,5 +1,8 @@ package code.bankconnectors +import code.api.util.APIUtil._ +import code.api.util.ApiRole._ +import code.api.util.ErrorMessages import code.management.ImporterAPI.ImporterTransaction import code.tesobe.CashTransaction import code.transactionrequests.TransactionRequests @@ -146,7 +149,7 @@ trait Connector { for { fromAccount <- getBankAccountType(fromAccountUID.bankId, fromAccountUID.accountId) ?~ s"account ${fromAccountUID.accountId} not found at bank ${fromAccountUID.bankId}" - isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") + isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccountUID.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true, ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) toAccount <- getBankAccountType(toAccountUID.bankId, toAccountUID.accountId) ?~ s"account ${toAccountUID.accountId} not found at bank ${toAccountUID.bankId}" //sameCurrency <- booleanToBox(fromAccount.currency == toAccount.currency, { @@ -257,7 +260,7 @@ trait Connector { var result = for { fromAccountType <- getBankAccountType(fromAccount.bankId, fromAccount.accountId) ?~ s"account ${fromAccount.accountId} not found at bank ${fromAccount.bankId}" - isOwner <- booleanToBox(initiator.ownerAccess(fromAccount), "user does not have access to owner view") + isOwner <- booleanToBox(initiator.ownerAccess(fromAccount) == true || hasEntitlement(fromAccount.bankId.value, initiator.userId, CanCreateAnyTransactionRequest) == true , ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) toAccountType <- getBankAccountType(toAccount.bankId, toAccount.accountId) ?~ s"account ${toAccount.accountId} not found at bank ${toAccount.bankId}" rawAmt <- tryo { BigDecimal(body.value.amount) } ?~! s"amount ${body.value.amount} not convertible to number" diff --git a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala index ad950b3cc..53d154b35 100644 --- a/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala +++ b/src/test/scala/code/api/LocalMappedConnectorTestSetup.scala @@ -5,8 +5,10 @@ import java.util.Date import bootstrap.liftweb.ToSchemify import code.model._ import code.model.dataAccess._ +import net.liftweb.common.Box import net.liftweb.mapper.MetaMapper import net.liftweb.util.Helpers._ +import code.entitlement.{MappedEntitlement, Entitlement} import scala.util.Random @@ -52,6 +54,16 @@ trait LocalMappedConnectorTestSetup extends TestConnectorSetupWithStandardPermis .accountLabel(randomString(4)).saveMe } + def addEntitlement(bankId: String, userId: String, roleName: String): Box[Entitlement] = { + // Return a Box so we can handle errors later. + val addEntitlement = MappedEntitlement.create + .mBankId(bankId) + .mUserId(userId) + .mRoleName(roleName) + .saveMe() + Some(addEntitlement) + } + override protected def createTransaction(account: BankAccount, startDate: Date, finishDate: Date) = { //ugly val mappedBankAccount = account.asInstanceOf[MappedBankAccount] diff --git a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala index 40033ace3..f9438dea6 100644 --- a/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala +++ b/src/test/scala/code/api/v2_0_0/TransactionRequestsTest.scala @@ -1,6 +1,7 @@ package code.api.v2_0_0 -import code.api.{DefaultUsers, ServerSetupWithTestData} +import code.api.util.ErrorMessages +import code.api.{ErrorMessage, DefaultUsers, ServerSetupWithTestData} import code.api.util.APIUtil.OAuth._ import code.api.v1_2_1.AmountOfMoneyJSON import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJSON} @@ -11,6 +12,7 @@ import net.liftweb.json.JsonAST.JString import net.liftweb.json.Serialization.write import net.liftweb.util.Props import org.scalatest.Tag +import code.api.util.ApiRole._ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers with V200ServerSetup { @@ -27,6 +29,154 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers }) } + // No challenge, No FX (same currencies) + if (Props.getBool("transactionRequests_enabled", false) == false) { + ignore("we create a transaction request without challenge, no FX (same currencies)", TransactionRequest) {} + } else { + scenario("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at BANK_ID", TransactionRequest) { + val testBank = createBank("transactions-test-bank") + val bankId = testBank.bankId + val accountId1 = AccountId("__acc1") + val accountId2 = AccountId("__acc2") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") + + addEntitlement(bankId.value, obpuser3.userId, CanCreateAnyTransactionRequest.toString) + Then("We add entitlement to user3") + val hasEntitlement = code.api.util.APIUtil.hasEntitlement(bankId.value, obpuser3.userId, CanCreateAnyTransactionRequest) + hasEntitlement should equal(true) + + def getFromAccount: BankAccount = { + BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) + } + + def getToAccount: BankAccount = { + BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) + } + + val fromAccount = getFromAccount + val toAccount = getToAccount + + val totalTransactionsBefore = transactionCount(fromAccount, toAccount) + + val beforeFromBalance = fromAccount.balance + val beforeToBalance = toAccount.balance + + //Create a transaction (request) + //1. get possible challenge types for from account + //2. create transaction request to to-account with one of the possible challenges + //3. answer challenge + //4. have a new transaction + + val transactionRequestId = TransactionRequestId("__trans1") + val toAccountJson = TransactionRequestAccountJSON(toAccount.bankId.value, toAccount.accountId.value) + + val amt = BigDecimal("12.50") + val bodyValue = AmountOfMoneyJSON("EUR", amt.toString()) + val transactionRequestBody = TransactionRequestBodyJSON(toAccountJson, bodyValue, "Test Transaction Request description") + + //call createTransactionRequest + var request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@(user3) + var response = makePostRequest(request, write(transactionRequestBody)) + Then("we should get a 201 created code") + response.code should equal(201) + + println(response.body) + + //created a transaction request, check some return values. As type is SANDBOX_TAN and value is < 1000, we expect no challenge + val transRequestId: String = (response.body \ "id") match { + case JString(i) => i + case _ => "" + } + Then("We should have some new transaction id") + transRequestId should not equal ("") + + val responseBody = response.body + + + val status: String = (response.body \ "status") match { + case JString(i) => i + case _ => "" + } + status should equal (code.transactionrequests.TransactionRequests.STATUS_COMPLETED) + + // Challenge should be null (none required) + var challenge = (response.body \ "challenge").children + challenge.size should equal(0) + + var transaction_ids = (response.body \ "transaction_ids") match { + case JString(i) => i + case _ => "" + } + //If user does not have access to owner or other view - they won’t be able to view transaction. Hence they can’t see the transaction_id + transaction_ids should not equal("") + + //call getTransactionRequests, check that we really created a transaction request + request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-requests").GET <@(user1) + response = makeGetRequest(request) + + Then("we should get a 200 ok code") + response.code should equal(200) + val transactionRequests = response.body.children + transactionRequests.size should not equal(0) + + + val tr2Body = response.body + + //check transaction_ids again + transaction_ids = (response.body \ "transaction_requests_with_charges" \ "transaction_ids") match { + case JString(i) => i + case _ => "" + } + transaction_ids should not equal("") + + //make sure that we also get no challenges back from this url (after getting from db) + challenge = (response.body \ "challenge").children + challenge.size should equal(0) + + //check that we created a new transaction (since no challenge) + request = (v1_4Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transactions").GET <@(user1) + response = makeGetRequest(request) + + Then("we should get a 200 ok code") + response.code should equal(200) + val transactions = response.body.children + + transactions.size should equal(1) + + //check that the description has been set + println(response.body) + /*val description = (((response.body \ "transactions")(0) \ "details") \ "description") match { + case JString(i) => i + case _ => "" + } + description should not equal ("")*/ + + //check that the balances have been properly decreased/increased (since we handle that logic for sandbox accounts at least) + //(do it here even though the payments test does test makePayment already) + val rate = fx.exchangeRate (fromAccount.currency, toAccount.currency) + val convertedAmount = fx.convert(amt, rate) + val fromAccountBalance = getFromAccount.balance + And("the from account should have a balance smaller by the amount specified to pay") + fromAccountBalance should equal((beforeFromBalance - convertedAmount)) + + /* + And("the newest transaction for the account receiving the payment should have the proper amount") + newestToAccountTransaction.details.value.amount should equal(amt.toString) + */ + + And("the account receiving the payment should have a new balance plus the amount paid") + val toAccountBalance = getToAccount.balance + toAccountBalance should equal(beforeToBalance + convertedAmount) + + And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") + transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) + } + } + // No challenge, No FX (same currencies) if (Props.getBool("transactionRequests_enabled", false) == false) { @@ -165,6 +315,109 @@ class TransactionRequestsTest extends ServerSetupWithTestData with DefaultUsers And("there should now be 2 new transactions in the database (one for the sender, one for the receiver") transactionCount(fromAccount, toAccount) should equal(totalTransactionsBefore + 2) } + + + scenario("we create a transaction request with a user without owner view access", TransactionRequest) { + val testBank = createBank("transactions-test-bank") + val bankId = testBank.bankId + val accountId1 = AccountId("__acc1") + val accountId2 = AccountId("__acc2") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") + + def getFromAccount: BankAccount = { + BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) + } + + def getToAccount: BankAccount = { + BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) + } + + val fromAccount = getFromAccount + val toAccount = getToAccount + + val toAccountJson = TransactionRequestAccountJSON(toAccount.bankId.value, toAccount.accountId.value) + + val amt = BigDecimal("12.50") + val bodyValue = AmountOfMoneyJSON("EUR", amt.toString()) + val transactionRequestBody = TransactionRequestBodyJSON(toAccountJson, bodyValue, "Test Transaction Request description") + + //call createTransactionRequest with a user without owner view access + val request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@(user2) + val response = makePostRequest(request, write(transactionRequestBody)) + Then("we should get a 400 created code") + response.code should equal(400) + + //created a transaction request, check some return values. As type is SANDBOX_TAN and value is < 1000, we expect no challenge + val error: String = (response.body \ "error") match { + case JString(i) => i + case _ => "" + } + Then("We should have the error: " + ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + error should equal (ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + + } + + + + + + scenario("we create a transaction request with a user who doesn't have access to owner view but has CanCreateAnyTransactionRequest at a different BANK_ID", TransactionRequest) { + val testBank = createBank("transactions-test-bank") + val testBank2 = createBank("transactions-test-bank2") + val bankId = testBank.bankId + val bankId2 = testBank2.bankId + val accountId1 = AccountId("__acc1") + val accountId2 = AccountId("__acc2") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId1, "EUR") + createAccountAndOwnerView(Some(obpuser1), bankId, accountId2, "EUR") + addEntitlement(bankId2.value, obpuser3.userId, CanCreateAnyTransactionRequest.toString) + + Then("We add entitlement to user3") + val hasEntitlement = code.api.util.APIUtil.hasEntitlement(bankId2.value, obpuser3.userId, CanCreateAnyTransactionRequest) + hasEntitlement should equal(true) + + def getFromAccount: BankAccount = { + BankAccount(bankId, accountId1).getOrElse(fail("couldn't get from account")) + } + + def getToAccount: BankAccount = { + BankAccount(bankId, accountId2).getOrElse(fail("couldn't get to account")) + } + + val fromAccount = getFromAccount + val toAccount = getToAccount + + val totalTransactionsBefore = transactionCount(fromAccount, toAccount) + + val beforeFromBalance = fromAccount.balance + val beforeToBalance = toAccount.balance + + val transactionRequestId = TransactionRequestId("__trans2") + val toAccountJson = TransactionRequestAccountJSON(toAccount.bankId.value, toAccount.accountId.value) + + val amt = BigDecimal("12.50") + val bodyValue = AmountOfMoneyJSON("EUR", amt.toString()) + val transactionRequestBody = TransactionRequestBodyJSON(toAccountJson, bodyValue, "Test Transaction Request description") + + //call createTransactionRequest + val request = (v2_0Request / "banks" / testBank.bankId.value / "accounts" / fromAccount.accountId.value / + "owner" / "transaction-request-types" / "SANDBOX_TAN" / "transaction-requests").POST <@ (user3) + val response = makePostRequest(request, write(transactionRequestBody)) + Then("we should get a 400 created code") + response.code should equal(400) + + //created a transaction request, check some return values. As type is SANDBOX_TAN and value is < 1000, we expect no challenge + val error: String = (response.body \ "error") match { + case JString(i) => i + case _ => "" + } + Then("We should have the error: " + ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + error should equal (ErrorMessages.InsufficientAuthorisationToCreateTransactionRequest) + + + } } // No challenge, with FX