diff options
author | Marcello Stanisci <stanisci.m@gmail.com> | 2017-12-22 21:55:56 +0100 |
---|---|---|
committer | Marcello Stanisci <stanisci.m@gmail.com> | 2017-12-22 21:55:56 +0100 |
commit | 4e1f605a97f48f6300f632b64f46b4d9e98714f5 (patch) | |
tree | 7df2771acd9aa0313ddf282c6e9d86e3befe6e66 /talerbank | |
parent | 9db330755e89eb233900474fcf454320eb5f678c (diff) | |
download | bank-4e1f605a97f48f6300f632b64f46b4d9e98714f5.tar.gz bank-4e1f605a97f48f6300f632b64f46b4d9e98714f5.tar.bz2 bank-4e1f605a97f48f6300f632b64f46b4d9e98714f5.zip |
Implementing #5222.
Diffstat (limited to 'talerbank')
-rw-r--r-- | talerbank/app/amount.py | 3 | ||||
-rw-r--r-- | talerbank/app/middleware.py | 62 | ||||
-rw-r--r-- | talerbank/app/models.py | 63 | ||||
-rw-r--r-- | talerbank/app/schemas.py | 77 | ||||
-rw-r--r-- | talerbank/app/templates/profile_page.html | 36 | ||||
-rw-r--r-- | talerbank/app/tests.py | 29 | ||||
-rw-r--r-- | talerbank/app/views.py | 423 | ||||
-rw-r--r-- | talerbank/settings.py | 1 |
8 files changed, 317 insertions, 377 deletions
diff --git a/talerbank/app/amount.py b/talerbank/app/amount.py index a36b880..b8447f8 100644 --- a/talerbank/app/amount.py +++ b/talerbank/app/amount.py @@ -25,10 +25,13 @@ from typing import Type class CurrencyMismatch(Exception): + hint = "Internal logic error (currency mismatch)" + http_status_code = 500 def __init__(self, curr1, curr2) -> None: super(CurrencyMismatch, self).__init__( "%s vs %s" % (curr1, curr2)) + class BadFormatAmount(Exception): def __init__(self, faulty_str) -> None: super(BadFormatAmount, self).__init__( diff --git a/talerbank/app/middleware.py b/talerbank/app/middleware.py new file mode 100644 index 0000000..ab4269a --- /dev/null +++ b/talerbank/app/middleware.py @@ -0,0 +1,62 @@ +import logging +from django.http import JsonResponse +from .models import BankAccount, BankTransaction +from .views import \ + (DebitLimitException, SameAccountException, + LoginFailed, RejectNoRightsException) +from .schemas import \ + (URLParameterMissing, URLParameterMalformed, + JSONFieldException, UnknownCurrencyException) +from .amount import CurrencyMismatch, BadFormatAmount + +LOGGER = logging.getLogger() + +EXCS = { + BankAccount.DoesNotExist: 0, + BankTransaction.DoesNotExist: 1, + SameAccountException: 2, + DebitLimitException: 3, + URLParameterMissing: 8, + URLParameterMalformed: 9, + JSONFieldException: 6, + CurrencyMismatch: 11, + BadFormatAmount: 11, + LoginFailed: 12, + RejectNoRightsException: 13, + UnknownCurrencyException: 14} + +APIS = { + "/reject": 5300, + "/history": 5200, + "/admin/add/incoming": 5100} + +RENDER = { + "/profile": "profile", + "/register": "index", + "/public-accounts": "index", + "/pin/verify": "profile"} + +class ExceptionMiddleware: + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + return self.get_response(request) + + def process_exception(self, request, exception): + if not EXCS.get(exception.__class__): + return None + taler_ec = EXCS.get(exception.__class__) + # The way error codes compose matches definitions found + # at [1]. + taler_ec += APIS.get(request.path, 1000) + render_to = RENDER.get(request.path) + if not render_to: + return JsonResponse({"ec": taler_ec, + "error": exception.hint}, + status=exception.http_status_code) + request.session["profile_hint"] = True, False, hint + return redirect(render_to) + +# [1] https://git.taler.net/exchange.git/tree/src/include/taler_error_codes.h#n1502 diff --git a/talerbank/app/models.py b/talerbank/app/models.py index f8c5c47..a584912 100644 --- a/talerbank/app/models.py +++ b/talerbank/app/models.py @@ -1,16 +1,18 @@ # This file is part of TALER # (C) 2014, 2015, 2016 INRIA # -# TALER 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. +# TALER 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. TALER 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 General Public License for more +# details. # -# TALER 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +# You should have received a copy of the GNU General Public License +# along with TALER; see the file COPYING. If not, see +# <http://www.gnu.org/licenses/> # # @author Marcello Stanisci # @author Florian Dold @@ -20,7 +22,9 @@ from typing import Any, Tuple from django.contrib.auth.models import User from django.db import models from django.conf import settings -from django.core.exceptions import ValidationError +from django.core.exceptions import \ + ValidationError, \ + ObjectDoesNotExist from .amount import Amount, BadFormatAmount class AmountField(models.Field): @@ -28,7 +32,8 @@ class AmountField(models.Field): description = 'Amount object in Taler style' def deconstruct(self) -> Tuple[str, str, list, dict]: - name, path, args, kwargs = super(AmountField, self).deconstruct() + name, path, args, kwargs = super( + AmountField, self).deconstruct() return name, path, args, kwargs def db_type(self, connection: Any) -> str: @@ -55,28 +60,42 @@ class AmountField(models.Field): return Amount.parse(settings.TALER_CURRENCY) return Amount.parse(value) except BadFormatAmount: - raise ValidationError("Invalid input for an amount string: %s" % value) + raise ValidationError( + "Invalid input for an amount string: %s" % value) def get_zero_amount() -> Amount: return Amount(settings.TALER_CURRENCY) +class BankAccountDoesNotExist(ObjectDoesNotExist): + hint = "Specified bank account does not exist" + http_status_code = 404 + +class BankTransactionDoesNotExist(ObjectDoesNotExist): + hint = "Specified bank transaction does not exist" + http_status_code = 404 + class BankAccount(models.Model): is_public = models.BooleanField(default=False) debit = models.BooleanField(default=False) account_no = models.AutoField(primary_key=True) user = models.OneToOneField(User, on_delete=models.CASCADE) amount = AmountField(default=get_zero_amount) + DoesNotExist = BankAccountDoesNotExist class BankTransaction(models.Model): amount = AmountField(default=False) - debit_account = models.ForeignKey(BankAccount, - on_delete=models.CASCADE, - db_index=True, - related_name="debit_account") - credit_account = models.ForeignKey(BankAccount, - on_delete=models.CASCADE, - db_index=True, - related_name="credit_account") - subject = models.CharField(default="(no subject given)", max_length=200) - date = models.DateTimeField(auto_now=True, db_index=True) + debit_account = models.ForeignKey( + BankAccount, + on_delete=models.CASCADE, + db_index=True, + related_name="debit_account") + credit_account = models.ForeignKey( + BankAccount, + on_delete=models.CASCADE, + db_index=True, + related_name="credit_account") + subject = models.CharField( + default="(no subject given)", max_length=200) + date = models.DateTimeField( + auto_now=True, db_index=True) cancelled = models.BooleanField(default=False) diff --git a/talerbank/app/schemas.py b/talerbank/app/schemas.py index 26d32ef..57473ef 100644 --- a/talerbank/app/schemas.py +++ b/talerbank/app/schemas.py @@ -20,9 +20,33 @@ definitions of JSON schemas for validating data """ import json -import validictory +from validictory import validate +from validictory.validator import \ + (RequiredFieldValidationError, + FieldValidationError) + from django.conf import settings +class UnknownCurrencyException(ValueError): + def __init__(self, hint, http_status_code): + self.hint = hint + self.http_status_code = http_status_code + +class URLParameterMissing(ValueError): + def __init__(self, param, http_status_code): + self.hint = "URL parameter '%s' is missing" % param + self.http_status_code = http_status_code + +class URLParameterMalformed(ValueError): + def __init__(self, param, http_status_code): + self.hint = "URL parameter '%s' is malformed" % param + self.http_status_code = http_status_code + +class JSONFieldException(ValueError): + def __init__(self, hint, http_status_code): + self.hint = hint + self.http_status_code = http_status_code + AMOUNT_SCHEMA = { "type": "object", "properties": { @@ -134,29 +158,50 @@ def validate_pintan_types(validator, fieldname, value, format_option): data = json.loads(value) validate_wiredetails(data) except Exception: - raise validictory.FieldValidationError( + raise FieldValidationError( "Malformed '%s'" % fieldname, fieldname, value) -def validate_pin_tan_args(pin_tan_args): +def validate_pin_tan(data): format_dict = { "str_to_int": validate_pintan_types, "wiredetails_string": validate_pintan_types} - validictory.validate(pin_tan_args, PIN_TAN_ARGS, format_validators=format_dict) + validate(data, PIN_TAN_ARGS, format_validators=format_dict) -def validate_reject_request(reject_request): - validictory.validate(reject_request, REJECT_REQUEST_SCHEMA) +def validate_reject(data): + validate(data, REJECT_REQUEST_SCHEMA) -def validate_history_request(history_request): - validictory.validate(history_request, HISTORY_REQUEST_SCHEMA) - -def validate_amount(amount): - validictory.validate(amount, AMOUNT_SCHEMA) +def validate_history(data): + validate(data, HISTORY_REQUEST_SCHEMA) def validate_wiredetails(wiredetails): - validictory.validate(wiredetails, WIREDETAILS_SCHEMA) + validate(wiredetails, WIREDETAILS_SCHEMA) + +def validate_add_incoming(data): + validate(data, INCOMING_REQUEST_SCHEMA) + +def check_withdraw_session(data): + validate(data, WITHDRAW_SESSION_SCHEMA) -def validate_incoming_request(incoming_request): - validictory.validate(incoming_request, INCOMING_REQUEST_SCHEMA) +def validate_data(request, data): + switch = { + "/reject": validate_reject, + "/history": validate_history, + "/admin/add/incoming": validate_add_incoming, + "/pin/verify": check_withdraw_session, + "/pin/question": validate_pin_tan + } + try: + switch.get(request.path)(data) + except RequiredFieldValidationError as exc: + if request.method == "GET": + raise URLParameterMissing(exc.fieldname, 400) + raise JSONFieldException( + "Field '%s' is missing" % exc.fieldname, 400) + except FieldValidationError as exc: + if exc.fieldname == "currency": + raise UnknownCurrencyException("Unknown currency", 406) + if request.method == "GET": + raise URLParameterMalformed(exc.fieldname, 400) + raise JSONFieldException( + "Malformed '%s' field" % exc.fieldname, 400) -def check_withdraw_session(session): - validictory.validate(session, WITHDRAW_SESSION_SCHEMA) diff --git a/talerbank/app/templates/profile_page.html b/talerbank/app/templates/profile_page.html index 6b465a0..4a03e52 100644 --- a/talerbank/app/templates/profile_page.html +++ b/talerbank/app/templates/profile_page.html @@ -42,38 +42,20 @@ </section> <section id="main"> <article> - <div class="notification"> - {% if wire_transfer_error %} + {% if fail_message %} + <div class="notification"> <p class="informational informational-fail"> - {% if info_bar %} - {{ info_bar }} - {% else %} - Could not perform wire transfer, check all fields are correctly - entered. - {% endif %} + {{ hint }} </p> - {% endif %} - {% if just_wire_transferred %} - <p class="informational informational-ok"> - Wire transfer done! - </p> - {% endif %} - {% if no_initial_bonus %} - <p class="informational informational-fail"> - No initial bonus given, poor bank! - </p> - {% endif %} - {% if just_withdrawn %} - <p class="informational informational-ok"> - Withdrawal approved! - </p> - {% endif %} - {% if just_registered %} + </div> + {% endif %} + {% if success_message %} + <div class="notification"> <p class="informational informational-ok"> - Registration successful! + {{ hint }} </p> - {% endif %} </div> + {% endif %} </article> <article> <div class="taler-installed-hide"> diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py index fb2d437..442888a 100644 --- a/talerbank/app/tests.py +++ b/talerbank/app/tests.py @@ -26,7 +26,7 @@ from mock import patch, MagicMock from urllib.parse import unquote from .models import BankAccount, BankTransaction from . import urls -from .views import wire_transfer +from .views import wire_transfer, LoginFailed from .amount import Amount, CurrencyMismatch, BadFormatAmount LOGGER = logging.getLogger() @@ -39,6 +39,7 @@ def clear_db(): with connection.cursor() as cursor: cursor.execute("ALTER SEQUENCE app_bankaccount_account_no_seq RESTART") cursor.execute("ALTER SEQUENCE app_banktransaction_id_seq RESTART") + class WithdrawTestCase(TestCase): def setUp(self): self.user_bank_account = BankAccount( @@ -106,9 +107,7 @@ class WithdrawTestCase(TestCase): args[0].dump() == amount.dump() \ and self.user_bank_account in args \ and "UVZ789" in args \ - and self.exchange_bank_account in args \ - and kwargs.get("session_expand") == \ - {"debt_limit": True}) + and self.exchange_bank_account in args) def tearDown(self): clear_db() @@ -193,16 +192,28 @@ class LoginTestCase(TestCase): user=User.objects.create_user( username="test_user", password="test_password")).save() + self.client = Client() def tearDown(self): clear_db() def test_login(self): - client = Client() - self.assertTrue(client.login(username="test_user", - password="test_password")) - self.assertFalse(client.login(username="test_user", - password="test_passwordii")) + self.assertTrue(self.client.login( + username="test_user", + password="test_password")) + self.assertFalse(self.client.login( + username="test_user", + password="test_passwordii")) + + def test_failing_login(self): + response = self.client.get( + reverse("history", urlconf=urls), {"auth": "basic"}, + **{"HTTP_X_TALER_BANK_USERNAME": "Wrong", + "HTTP_X_TALER_BANK_PASSWORD": "Credentials"}) + data = response.content.decode("utf-8") + self.assertJSONEqual('{"error": "Wrong username/password", "ec": 5212}', json.loads(data)) + self.assertEqual(401, response.status_code) + class AmountTestCase(TestCase): diff --git a/talerbank/app/views.py b/talerbank/app/views.py index a231402..7f6093d 100644 --- a/talerbank/app/views.py +++ b/talerbank/app/views.py @@ -42,24 +42,27 @@ from django.db.models import Q from django.http import (JsonResponse, HttpResponse, HttpResponseBadRequest as HRBR) from django.shortcuts import render, redirect -from validictory.validator import \ - (RequiredFieldValidationError as RFVE, - FieldValidationError as FVE) from .models import BankAccount, BankTransaction from .amount import Amount, CurrencyMismatch, BadFormatAmount -from .schemas import (validate_pin_tan_args, check_withdraw_session, - validate_history_request, - validate_incoming_request, - validate_reject_request) - +from .schemas import validate_data LOGGER = logging.getLogger(__name__) -class DebtLimitExceededException(Exception): - def __init__(self) -> None: - super().__init__("Debt limit exceeded") +class LoginFailed(Exception): + hint = "Wrong username/password" + http_status_code = 401 + +class DebitLimitException(Exception): + hint = "Debit too high, operation forbidden." + http_status_code = 403 class SameAccountException(Exception): - pass + hint = "Debit and credit account are the same." + http_status_code = 403 + +class RejectNoRightsException(Exception): + hint = "You weren't the transaction credit account, " \ + "no rights to reject." + http_status_code = 403 class MyAuthenticationForm( django.contrib.auth.forms.AuthenticationForm): @@ -89,9 +92,10 @@ def get_session_flag(request, name): Get a flag from the session and clear it. """ if name in request.session: + ret = request.session[name] del request.session[name] - return True - return False + return ret + return False, False, None def predefined_accounts_list(): @@ -142,58 +146,33 @@ def profile_page(request): if wtf.is_valid(): amount_parts = (settings.TALER_CURRENCY, wtf.cleaned_data.get("amount") + 0.0) - try: - wire_transfer( - Amount.parse("%s:%s" % amount_parts), - BankAccount.objects.get( - user=request.user), - BankAccount.objects.get( - account_no=wtf.cleaned_data.get( - "receiver")), - wtf.cleaned_data.get("subject")) - request.session["just_wire_transferred"] = True - except BankAccount.DoesNotExist: - request.session["wire_transfer_error"] = True - info_bar = "Specified account for receiver does not" \ - " exist" - except WireTransferException as exc: - request.session["wire_transfer_error"] = True - info_bar = "Internal server error, sorry!" - if isinstance(exc.exc, SameAccountException): - info_bar = "Operation not possible:" \ - " debit and credit account" \ - " are the same!" - else: - LOGGER.warning("invalid wire transfer POSTed", wtf.errors) + wire_transfer( + Amount.parse("%s:%s" % amount_parts), + BankAccount.objects.get(user=request.user), + BankAccount.objects.get(account_no=wtf.cleaned_data.get("receiver")), + wtf.cleaned_data.get("subject")) wtf = WTForm() - - just_withdrawn = get_session_flag(request, "just_withdrawn") + fail_message, success_message, hint = get_session_flag(request, "profile_hint") context = dict( name=request.user.username, balance=request.user.bankaccount.amount.stringify( settings.TALER_DIGITS, pretty=True), sign="-" if request.user.bankaccount.debit else "", + fail_message=fail_message, + success_message=success_message, + hint=hint, precision=settings.TALER_DIGITS, currency=request.user.bankaccount.amount.currency, account_no=request.user.bankaccount.account_no, wt_form=wtf, history=extract_history(request.user.bankaccount), - just_withdrawn=just_withdrawn, - just_registered=get_session_flag( - request, "just_registered"), - no_initial_bonus=get_session_flag( - request, "no_initial_bonus"), - just_wire_transferred=get_session_flag( - request, "just_wire_transferred"), - wire_transfer_error=get_session_flag( - request, "wire_transfer_error"), - info_bar=info_bar ) if settings.TALER_SUGGESTED_EXCHANGE: context["suggested_exchange"] = settings.TALER_SUGGESTED_EXCHANGE response = render(request, "profile_page.html", context) - if just_withdrawn: + if "just_withdrawn" in request.session: + del request.session["just_withdrawn"] response["X-Taler-Operation"] = "confirm-reserve" response["X-Taler-Reserve-Pub"] = request.session.get( "reserve_pub") @@ -226,21 +205,7 @@ def make_question(): @require_GET @login_required def pin_tan_question(request): - try: - validate_pin_tan_args(request.GET.dict()) - # Currency is not checked, as any mismatches will be - # detected afterwards - except (FVE, RFVE) as err: - ec = settings.TALER_EC_PARAMETER_MALFORMED - if isinstance(err, RFVE): - ec = settings.TALER_EC_PARAMETER_MISSING - LOGGER.error( - "missing/malformed parameter '%s'" % err.fieldname) - return JsonResponse( - {"error": "missing parameter '%s'" % err.fieldname, - "ec": ec}, - status=400) - + validate_data(request, request.GET.dict()) user_account = BankAccount.objects.get(user=request.user) wd = json.loads(request.GET["exchange_wire_details"]) request.session["exchange_account_number"] = \ @@ -278,29 +243,16 @@ def pin_tan_verify(request): request.session["captcha_failed"] = True return redirect(request.POST.get("question_url", "profile")) # Check the session is a "pin tan" one - try: - check_withdraw_session(request.session) - amount = Amount(**request.session["amount"]) - exchange_bank_account = BankAccount.objects.get( - account_no=request.session["exchange_account_number"]) - wire_transfer(amount, - BankAccount.objects.get(user=request.user), - exchange_bank_account, - request.session["reserve_pub"], - request=request, - session_expand=dict(debt_limit=True)) - except (FVE, RFVE) as exc: - LOGGER.warning("Not a withdrawing session") - return redirect("profile") - - except BankAccount.DoesNotExist as exc: - return JsonResponse( - {"error": "That exchange is unknown to this bank", - "ec": settings.TALER_EC_BANK_ACCOUNT_NOT_FOUND}, - status=404) - except WireTransferException as exc: - return exc.response - request.session["just_withdrawn"] = True + validate_data(request, request.session) + amount = Amount(**request.session["amount"]) + exchange_bank_account = BankAccount.objects.get( + account_no=request.session["exchange_account_number"]) + wire_transfer(amount, + BankAccount.objects.get(user=request.user), + exchange_bank_account, + request.session["reserve_pub"]) + request.session["profile_hint"] = False, True, "Withdrawal successful!" + request.session["just_withdraw"] = True return redirect("profile") class UserReg(forms.Form): @@ -330,16 +282,11 @@ def register(request): user_account = BankAccount(user=user) user_account.save() bank_internal_account = BankAccount.objects.get(account_no=1) - try: - wire_transfer(Amount(settings.TALER_CURRENCY, 100, 0), - bank_internal_account, - user_account, - "Joining bonus", - request=request, - session_expand=dict(no_initial_bobus=True)) - except WireTransferException as exc: - return exc.response - request.session["just_registered"] = True + wire_transfer(Amount(settings.TALER_CURRENCY, 100, 0), + bank_internal_account, + user_account, + "Joining bonus") + request.session["profile_hint"] = False, True, "Registration successful!" user = django.contrib.auth.authenticate( username=username, password=password) django.contrib.auth.login(request, user) @@ -383,13 +330,7 @@ def extract_history(account): def serve_public_accounts(request, name=None): if not name: name = settings.TALER_PREDEFINED_ACCOUNTS[0] - try: user = User.objects.get(username=name) - except User.DoesNotExist: - return HttpResponse( - {"error": "account '%s' not found" % name, - "ec": settings.TALER_EC_BANK_UNKNOWN_ACCOUNT}, - status=404) public_accounts = BankAccount.objects.filter(is_public=True) history = extract_history(account) context = dict( @@ -407,15 +348,7 @@ def login_via_headers(view_func): user_account = auth_and_login(request) if not user_account: LOGGER.error("authentication failed") - switch = { - "serve_history": - settings.TALER_EC_BANK_HISTORY_NOT_AUTHORIZED} - ec = switch.get(view_func.__name, - settings.TALER_EC_BANK_NOT_AUTHORIZED) - return JsonResponse( - {"error": "authentication failed", - "ec": ec}, - status=401) + raise LoginFailed("authentication failed") return view_func(request, user_account, *args, **kwargs) return wraps(view_func)(_decorator) @@ -426,19 +359,7 @@ def serve_history(request, user_account): This API is used to get a list of transactions related to one user. """ - try: - # Note, this does check the currency. - validate_history_request(request.GET.dict()) - except (FVE, RFVE) as exc: - ec = settings.TALER_EC_PARAMETER_MALFORMED - if isinstance(exc, RFVE): - ec = settings.TALER_EC_PARAMETER_MISSING - LOGGER.error("/history, bad '%s' arg" % exc.fieldname) - return JsonResponse( - {"error": "invalid '%s'" % exc.fieldname, - "ec": ec}, - status=400) - + validate_data(request, request.GET.dict()) # delta parsed_delta = re.search(r"([\+-])?([0-9]+)", request.GET.get("delta")) @@ -507,14 +428,13 @@ def auth_and_login(request): auth_type = request.GET.get("auth") if auth_type != "basic": LOGGER.error("auth method not supported") - return False + raise LoginFailed("auth method not supported") username = request.META.get("HTTP_X_TALER_BANK_USERNAME") password = request.META.get("HTTP_X_TALER_BANK_PASSWORD") - LOGGER.info("Trying to log '%s/%s' in" % (username, password)) if not username or not password: LOGGER.error("user or password not given") - return False + raise LoginFailed("missing user/password") return django.contrib.auth.authenticate( username=username, password=password) @@ -525,32 +445,11 @@ def auth_and_login(request): @login_via_headers def reject(request, user_account): data = json.loads(request.body.decode("utf-8")) - try: - validate_reject_request(data) - except (FVE, RFVE) as exc: - ec = settings.TALER_EC_PARAMETER_MALFORMED - if isinstance(err, RFVE): - ec = settings.TALER_EC_PARAMETER_MISSING - LOGGER.error("invalid %s" % exc.fieldname) - return JsonResponse( - {"error": "invalid '%s'" % exc.fieldname, - "ec": ec}, - status=400) - try: - trans = BankTransaction.objects.get(id=data["row_id"]) - except BankTransaction.DoesNotExist: - return JsonResponse( - {"error": "unknown transaction", - "ec": settings.TALER_EC_BANK_REJECT_NOT_FOUND}, - status=404) + validate_data(request, data) + trans = BankTransaction.objects.get(id=data["row_id"]) if trans.credit_account.account_no != \ user_account.bankaccount.account_no: - LOGGER.error("you can only reject a transaction where you" - " _got_ money") - return JsonResponse( - {"error": "no rights to reject", - "ec": settings.TALER_EC_BANK_REJECT_NO_RIGHTS}, - status=401) # Unauthorized + raise RejectNoRightsException() trans.cancelled = True trans.debit_account.amount.add(trans.amount) trans.credit_account.amount.subtract(trans.amount) @@ -570,37 +469,14 @@ def add_incoming(request, user_account): within the browser, and only over the private admin interface. """ data = json.loads(request.body.decode("utf-8")) - try: - # Note, this does check the currency. - validate_incoming_request(data) - except (FVE, RFVE) as exc: - ec = settings.TALER_EC_PARAMETER_MALFORMED - if isinstance(exc, RFVE): - ec = settings.TALER_EC_PARAMETER_MISSING - LOGGER.error( - "missing/malformed parameter '%s'" % exc.fieldname) - return JsonResponse( - {"error": "missing parameter '%s'" % exc.fieldname, - "ec": ec}, - status=406 if exc.fieldname == "currency" else 400) - + validate_data(request, data) subject = "%s %s" % (data["subject"], data["exchange_url"]) - try: - credit_account = BankAccount.objects.get( - account_no=data["credit_account"]) - wtrans = wire_transfer(Amount(**data["amount"]), - user_account.bankaccount, - credit_account, - subject) - except BankAccount.DoesNotExist: - return JsonResponse( - {"error": - "credit_account (%d) not found" \ - % data["credit_account"], - "ec": settings.TALER_EC_BANK_UNKNOWN_ACCOUNT}, - status=404) - except WireTransferException as exc: - return exc.response + credit_account = BankAccount.objects.get( + account_no=data["credit_account"]) + wtrans = wire_transfer(Amount(**data["amount"]), + user_account.bankaccount, + credit_account, + subject) return JsonResponse( {"row_id": wtrans.id, "timestamp": "/Date(%s)/" % int(wtrans.date.timestamp())}) @@ -610,21 +486,9 @@ def add_incoming(request, user_account): @require_POST def withdraw_nojs(request): - try: - amount = Amount.parse( - request.POST.get("kudos_amount", "not-given")) - except BadFormatAmount: - LOGGER.error("Amount ('%s') did not pass parsing" % amount) - ec = settings.TALER_EC_PARAMETER_MALFORMED - if amount == "not-given": - ec = settings.TALER_EC_PARAMETER_MISSING - return JsonResponse({ - "error": "bad 'kudos_amount' parameter", - "ec": ec}, - status=400) - + amount = Amount.parse( + request.POST.get("kudos_amount", "not-given")) user_account = BankAccount.objects.get(user=request.user) - response = HttpResponse(status=202) response["X-Taler-Operation"] = "create-reserve" response["X-Taler-Callback-Url"] = reverse("pin-question") @@ -640,112 +504,65 @@ def withdraw_nojs(request): settings.TALER_SUGGESTED_EXCHANGE return response -class WireTransferException(Exception): - def __init__(self, exc, response): - self.exc = exc - self.response = response - super().__init__() - -def wire_transfer(amount, - debit_account, - credit_account, - subject, - **kwargs): - def err_cb(exc, resp): - LOGGER.error(str(exc)) - raise WireTransferException(exc, resp) - - def wire_transfer_internal(amount, - debit_account, - credit_account, - subject): - LOGGER.info("%s => %s, %s, %s" % - (debit_account.account_no, - credit_account.account_no, - amount.stringify(2), - subject)) - if debit_account.pk == credit_account.pk: - LOGGER.error("Debit and credit account are the same!") - raise SameAccountException() - - transaction_item = BankTransaction( - amount=amount, - credit_account=credit_account, - debit_account=debit_account, - subject=subject) - if debit_account.debit: - debit_account.amount.add(amount) - - elif -1 == Amount.cmp(debit_account.amount, amount): - debit_account.debit = True - tmp = Amount(**amount.dump()) - tmp.subtract(debit_account.amount) - debit_account.amount.set(**tmp.dump()) - else: - debit_account.amount.subtract(amount) - - if not credit_account.debit: - credit_account.amount.add(amount) - elif Amount.cmp(amount, credit_account.amount) == 1: - credit_account.debit = False - tmp = Amount(**amount.dump()) - tmp.subtract(credit_account.amount) - credit_account.amount.set(**tmp.dump()) - else: - credit_account.amount.subtract(amount) - - # Check here if any account went beyond the allowed - # debit threshold. - - threshold = Amount.parse(settings.TALER_MAX_DEBT) - if debit_account.user.username == "Bank": - threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK) - if Amount.cmp(debit_account.amount, threshold) == 1 \ - and Amount.cmp(Amount(settings.TALER_CURRENCY), - threshold) != 0 \ - and debit_account.debit: - LOGGER.info("Negative balance '%s' not allowed.\ - " % json.dumps(debit_account.amount.dump())) - LOGGER.info("%s's threshold is: '%s'." \ - % (debit_account.user.username, - json.dumps(threshold.dump()))) - raise DebtLimitExceededException() - - with transaction.atomic(): - debit_account.save() - credit_account.save() - transaction_item.save() - - return transaction_item - - try: - return wire_transfer_internal( - amount, - debit_account, - credit_account, - subject) - except (CurrencyMismatch, BadFormatAmount) as exc: - err_cb(exc, JsonResponse( - {"error": "internal server error", - "ec": settings.TALER_EC_INTERNAL_LOGIC_ERROR}, - status=500)) - except DebtLimitExceededException as exc: - if kwargs.get("request"): - if kwargs.get("session_expand"): - kwargs["request"].session.update( - kwargs["session_expand"]) - if kwargs["request"].request.path == "/pin/verify": - err_cb(exc, redirect("profile")) - else: - err_cb(exc, JsonResponse( - {"error": "Unallowed debit", - "ec": settings.TALER_EC_BANK_TRANSFER_DEBIT}, - status=403)) - except SameAccountException as exc: - err_cb(exc, JsonResponse( - {"error": "sender account == receiver account", - "ec": settings.TALER_EC_BANK_TRANSFER_SAME_ACCOUNT}, - status=422)) +def wire_transfer(amount, debit_account, credit_account, + subject): + LOGGER.info("%s => %s, %s, %s" % + (debit_account.account_no, + credit_account.account_no, + amount.stringify(2), + subject)) + if debit_account.pk == credit_account.pk: + LOGGER.error("Debit and credit account are the same!") + raise SameAccountException() + + transaction_item = BankTransaction( + amount=amount, + credit_account=credit_account, + debit_account=debit_account, + subject=subject) + if debit_account.debit: + debit_account.amount.add(amount) + + elif -1 == Amount.cmp(debit_account.amount, amount): + debit_account.debit = True + tmp = Amount(**amount.dump()) + tmp.subtract(debit_account.amount) + debit_account.amount.set(**tmp.dump()) + else: + debit_account.amount.subtract(amount) + + if not credit_account.debit: + credit_account.amount.add(amount) + elif Amount.cmp(amount, credit_account.amount) == 1: + credit_account.debit = False + tmp = Amount(**amount.dump()) + tmp.subtract(credit_account.amount) + credit_account.amount.set(**tmp.dump()) + else: + credit_account.amount.subtract(amount) + + # Check here if any account went beyond the allowed + # debit threshold. + + threshold = Amount.parse(settings.TALER_MAX_DEBT) + if debit_account.user.username == "Bank": + threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK) + if Amount.cmp(debit_account.amount, threshold) == 1 \ + and Amount.cmp(Amount(settings.TALER_CURRENCY), + threshold) != 0 \ + and debit_account.debit: + LOGGER.info("Negative balance '%s' not allowed.\ + " % json.dumps(debit_account.amount.dump())) + LOGGER.info("%s's threshold is: '%s'." \ + % (debit_account.user.username, + json.dumps(threshold.dump()))) + raise DebitLimitException() + + with transaction.atomic(): + debit_account.save() + credit_account.save() + transaction_item.save() + return transaction_item # [1] https://stackoverflow.com/questions/24783275/django-form-with-choices-but-also-with-freetext-option diff --git a/talerbank/settings.py b/talerbank/settings.py index 77e3c4f..07b8124 100644 --- a/talerbank/settings.py +++ b/talerbank/settings.py @@ -71,6 +71,7 @@ MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'talerbank.app.middleware.ExceptionMiddleware', ] TEMPLATES = [ |