## # 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 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 # # # @author Marcello Stanisci # @brief definitions of JSON schemas for validating data import json from validictory import validate from validictory.validator import \ (RequiredFieldValidationError, FieldValidationError) from django.conf import settings ## # Exception class to be raised when a expected URL parameter # is not found. class URLParameterMissing(ValueError): ## # Init method. # # @param self the object itself. # @param param the missing URL parameter name. # @param http_status_code the HTTP response code to return # to the caller (client). def __init__(self, param, http_status_code): self.hint = "URL parameter '%s' is missing" % param self.http_status_code = http_status_code super().__init__() ## # Exception class to be raised when a expected URL parameter # is malformed. class URLParameterMalformed(ValueError): ## # Init method. # # @param self the object itself. # @param param the malformed URL parameter name. # @param http_status_code the HTTP response code to return # to the caller (client). def __init__(self, param, http_status_code): self.hint = "URL parameter '%s' is malformed" % param self.http_status_code = http_status_code super().__init__() ## # Exception class to be raised when a JSON # object does not respect a specification. class JSONFieldException(ValueError): ## # Init method. # # @param self the object itself. # @param hint the hint to be displayed along the error. # @param http_status_code HTTP response code to be returned # along the error. def __init__(self, hint, http_status_code): self.hint = hint self.http_status_code = http_status_code super().__init__() ## # Pattern for amounts. AMOUNT_SCHEMA = { "type": "string", "pattern": "^[A-Za-z0-9_-]+:([0-9]+)\.?([0-9]+)?$"} ## # Definition that withdraw request bodies have to match. WITHDRAW_SESSION_SCHEMA = { "type": "object", "properties": { "amount": {"type": AMOUNT_SCHEMA}, "reserve_pub": {"type": "string"}, "exchange_account_number": {"type": "integer"}, "sender_wiredetails": { "type": "object", "properties": { "type": {"type": "string"}, "bank_url": {"type": "string"}, "account_number": {"type": "integer"} } } } } ## # Definition for wire details. WIREDETAILS_SCHEMA = { "type": "object", "properties": { "test": { "type": "object", "properties": { "type": {"type": "string"}, "account_number": {"type": "integer"}, "bank_url": {"type": "string"}, "name": {"type": "string", "required": False}, } } } } ## # Definition for authentication objects. AUTH_SCHEMA = { "type": "object", "properties": { "type": {"type": "string", "pattern": "^basic$"}, "data": {"type": "object", "required": False} } } ## # Definition for reject request bodies. REJECT_REQUEST_SCHEMA = { "type": "object", "properties": { "auth": AUTH_SCHEMA, "row_id": {"type": "integer"}, "account_number": {"type": "integer"} } } ## # Definition for /history-range request URL parameters. HISTORY_RANGE_REQUEST_SCHEMA = { "type": "object", "properties": { "auth": {"type": "string", "pattern": "^basic$"}, "cancelled": {"type": "string", "pattern": "^(omit|show)$", "required": False}, "start": {"type": "string", "pattern": r"^[0-9]+$"}, "end": {"type": "string", "pattern": r"^[0-9]+$"}, "ordering": {"type": "string", "pattern": r"^(ascending|descending)$", "required": False}, "direction": {"type": "string", "pattern": r"^(debit|credit|both|cancel\+|cancel-)$"}, "account_number": {"type": "string", "pattern": "^([0-9]+)$", "required": False} } } ## # Definition for /history request URL parameters. HISTORY_REQUEST_SCHEMA = { "type": "object", "properties": { "auth": {"type": "string", "pattern": "^basic$"}, "cancelled": {"type": "string", "pattern": "^(omit|show)$", "required": False}, "delta": {"type": "string", "pattern": r"^([\+-])?([0-9])+$"}, "start": {"type": "string", "pattern": "^([0-9]+)$", "required": False}, "ordering": {"type": "string", "pattern": r"^(ascending|descending)$", "required": False}, "direction": {"type": "string", "pattern": r"^(debit|credit|both|cancel\+|cancel-)$"}, "account_number": {"type": "string", "pattern": "^([0-9]+)$", "required": False} } } ## # Definition for /add/incoming request bodies. INCOMING_REQUEST_SCHEMA = { "type": "object", "properties": { "amount": {"type": AMOUNT_SCHEMA}, "subject": {"type": "string"}, "credit_account": {"type": "integer"}, "auth": AUTH_SCHEMA } } ## # Definition for PIN/TAN request URL parameters. PIN_TAN_ARGS = { "type": "object", "properties": { "amount_value": {"format": "str_to_int"}, "amount_fraction": {"format": "str_to_int"}, "amount_currency": {"type": "string"}, "exchange": {"type": "string"}, "reserve_pub": {"type": "string"}, "exchange_wire_details": {"format": "string"} } } ## # Check the custom types for a PIN/TAN request. Those # types the strings that must represent integers (like # those used for amount's values and fractions), and the # stringification of a wire details object. All of them # get passed along the GET request's arguments by the # wallet. # # @param validator unused. # @param fieldname current name of the field being processed. # @param value current name of the value being processed. # @param format_option holds the format definition for the # current field being processed. def validate_pintan_types(validator, fieldname, value, format_option): del validator # pacify PEP checkers try: if format_option == "str_to_int": int(value) if format_option == "wiredetails_string": data = json.loads(value) validate_wiredetails(data) except Exception: raise FieldValidationError( "Malformed '%s'" % fieldname, fieldname, value) ## # Check the GET arguments given along a /pin/question request. # # @param data GET arguments in a dict. def validate_pin_tan(data): format_dict = { "str_to_int": validate_pintan_types, "wiredetails_string": validate_pintan_types} validate(data, PIN_TAN_ARGS, format_validators=format_dict) ## # Check if the /reject request is valid. # # @param data POST/PUT body. def validate_reject(data): validate(data, REJECT_REQUEST_SCHEMA) ## # Check /history input data. # # @param data dict representing /history's GET parameters. def validate_history(data): validate(data, HISTORY_REQUEST_SCHEMA) ## # Check /history-range input data. # # @param data dict representing /history's GET parameters. def validate_history_range(data): validate(data, HISTORY_RANGE_REQUEST_SCHEMA) ## # Check wire details # (regardless of which endpoint triggered the check) # # @param wiredetails object representing wire details. def validate_wiredetails(wiredetails): validate(wiredetails, WIREDETAILS_SCHEMA) ## # Check input data for a wire transfer commanded via the # HTTP REST service. # # @param data POST body sent along the request. def validate_add_incoming(data): validate(data, INCOMING_REQUEST_SCHEMA) ## # Check that the state corresponds to a withdrawal session. # # @param data the dict representing the server state. So not # strictly an 'input' sent by any client; we use this just # as a double-checking mechanism to see if the server is # well-behaved. def check_withdraw_session(data): validate(data, WITHDRAW_SESSION_SCHEMA) ## # Abstraction over the real validators. Do not return, # but rather raise exceptions if the data is invalid. # # @param request Django-specific HTTP request object. # @param data data to validate. May be a POST body or # a dict holding the param-value pairs from a GET # request. def validate_data(request, data): switch = { "/reject": validate_reject, "/history": validate_history, "/history-range": validate_history_range, "/admin/add/incoming": validate_add_incoming, "/pin/verify": check_withdraw_session, "/pin/question": validate_pin_tan } try: switch.get(request.path_info)(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 request.method == "GET": raise URLParameterMalformed(exc.fieldname, 400) raise JSONFieldException( "Malformed '%s' field" % exc.fieldname, 400)