Skip to content

Commit

Permalink
Merge pull request #135 from bcgov/development
Browse files Browse the repository at this point in the history
Merge development to master (release 1.4.0)
  • Loading branch information
sumesh-aot authored Dec 17, 2019
2 parents f5f9600 + 5436e6d commit 5f23be1
Show file tree
Hide file tree
Showing 25 changed files with 235 additions and 164 deletions.
3 changes: 2 additions & 1 deletion openshift/templates/api-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"NATS_SUBJECT": "entity.filing.payment",
"NATS_QUEUE": "filing-worker",
"BCOL_PAYMENTS_WSDL_URL": "${BCOL_PAYMENTS_WSDL_URL}",
"BCOL_API_ENDPOINT": "https://${BCOL_API_NAME}-${TAG_NAME}.pathfinder.gov.bc.ca/api/v1/reports"
"BCOL_API_ENDPOINT": "https://${BCOL_API_NAME}-${TAG_NAME}.pathfinder.gov.bc.ca/api/v1/reports",
"VALID_REDIRECT_URLS": "https://${AUTH_WEB_NAME}-${TAG_NAME}.pathfinder.gov.bc.ca/cooperatives/*"
}
}
],
Expand Down
7 changes: 6 additions & 1 deletion pay-api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _get_config(config_key: str, **kwargs):
value = os.getenv(config_key, kwargs.get('default'))
else:
value = os.getenv(config_key)
assert value
# assert value TODO Un-comment once we find a solution to run pre-hook without initializing app
return value


Expand Down Expand Up @@ -118,6 +118,9 @@ class _Config(object): # pylint: disable=too-few-public-methods
# BCOL Service
BCOL_API_ENDPOINT = _get_config('BCOL_API_ENDPOINT')

# Valid Payment redirect URLs
VALID_REDIRECT_URLS = [(val.strip() if val != '' else None) for val in _get_config('VALID_REDIRECT_URLS', default='').split(',')]

TESTING = False
DEBUG = True

Expand Down Expand Up @@ -235,6 +238,8 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods

BCOL_API_ENDPOINT = 'https://mock-lear-tools.pathfinder.gov.bc.ca/rest/bcol-api-1.0.0.yaml/1.0'

VALID_REDIRECT_URLS = ['http://localhost:8080/*']


class ProdConfig(_Config): # pylint: disable=too-few-public-methods
"""Production environment configuration."""
Expand Down
6 changes: 2 additions & 4 deletions pay-api/src/pay_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,11 @@

import os

import sentry_sdk
from flask import Flask
from sbc_common_components.exception_handling.exception_handler import ExceptionHandler
from sbc_common_components.utils.camel_case_response import convert_to_camel
from sentry_sdk.integrations.flask import FlaskIntegration # noqa: I001

import sentry_sdk # noqa: I001; pylint: disable=ungrouped-imports; conflicts with Flake8
from sentry_sdk.integrations.flask import FlaskIntegration

import config
from config import _Config
Expand All @@ -33,7 +32,6 @@
from pay_api.utils.run_version import get_run_version


# important to do this first
setup_logging(os.path.join(_Config.PROJECT_ROOT, 'logging.conf'))


Expand Down
4 changes: 2 additions & 2 deletions pay-api/src/pay_api/factory/payment_system_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
from pay_api.exceptions import BusinessException
from pay_api.services.base_payment_system import PaymentSystemService
from pay_api.services.internal_pay_service import InternalPayService
from pay_api.services.bcol_service import BcolService
from pay_api.services.bcol_service import BcolService # noqa: I001
from pay_api.services.paybc_service import PaybcService
from pay_api.utils.enums import Role, PaymentSystem
from pay_api.utils.enums import Role, PaymentSystem # noqa: I001
from pay_api.utils.errors import Error


Expand Down
2 changes: 1 addition & 1 deletion pay-api/src/pay_api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from .fee_code import FeeCode, FeeCodeSchema
from .fee_schedule import FeeSchedule, FeeScheduleSchema
from .filing_type import FilingType, FilingTypeSchema
from .payment_account import PaymentAccount, PaymentAccountSchema
from .payment_account import PaymentAccount, PaymentAccountSchema # noqa: I001
from .invoice import Invoice, InvoiceSchema
from .invoice_reference import InvoiceReference, InvoiceReferenceSchema
from .payment import Payment, PaymentSchema
Expand Down
10 changes: 3 additions & 7 deletions pay-api/src/pay_api/resources/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"""Resource for Transaction endpoints."""
from http import HTTPStatus

import flask
from flask import current_app, jsonify, request
from flask_restplus import Namespace, Resource, cors

Expand All @@ -36,7 +35,6 @@ class Transaction(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@_jwt.requires_auth
@_tracing.trace()
def post(payment_id):
"""Create the Transaction records."""
Expand All @@ -50,8 +48,7 @@ def post(payment_id):
return jsonify({'code': 'PAY007', 'message': schema_utils.serialize(errors)}), HTTPStatus.BAD_REQUEST

try:
response, status = TransactionService.create(payment_id, request_json,
jwt=_jwt).asdict(), HTTPStatus.CREATED
response, status = TransactionService.create(payment_id, request_json).asdict(), HTTPStatus.CREATED
except BusinessException as exception:
response, status = {'code': exception.code, 'message': exception.message}, exception.status
current_app.logger.debug('>Transaction.post')
Expand Down Expand Up @@ -92,16 +89,15 @@ def get(payment_id, transaction_id):

@staticmethod
@cors.crossdomain(origin='*')
@_jwt.requires_auth
@_tracing.trace()
def patch(payment_id, transaction_id):
"""Update the transaction record by querying payment system."""
current_app.logger.info(
f'<Transaction.post for payment : {payment_id}, and transaction {transaction_id}')
receipt_number = flask.request.args.get('receipt_number')
receipt_number = request.get_json().get('receipt_number', None)
try:
response, status = TransactionService.update_transaction(payment_id, transaction_id,
receipt_number, _jwt).asdict(), HTTPStatus.OK
receipt_number).asdict(), HTTPStatus.OK
except BusinessException as exception:
response, status = {'code': exception.code, 'message': exception.message}, exception.status
current_app.logger.debug('>Transaction.post')
Expand Down
25 changes: 14 additions & 11 deletions pay-api/src/pay_api/services/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,25 @@
from flask_jwt_oidc import JwtManager

from pay_api.services.oauth_service import OAuthService as RestService
from pay_api.utils.enums import AuthHeaderType, ContentType
from pay_api.utils.enums import AuthHeaderType, ContentType, Role


def check_auth(business_identifier: str, jwt: JwtManager, **kwargs):
"""Authorize the user for the business entity."""
bearer_token = jwt.get_token_auth_header() if jwt else None
auth_url = current_app.config.get('AUTH_API_ENDPOINT') + f'entities/{business_identifier}/authorizations'
auth_response = RestService.get(auth_url, bearer_token, AuthHeaderType.BEARER, ContentType.JSON)

is_authorized: bool = False
if auth_response:
roles: list = auth_response.json().get('roles', [])
if kwargs.get('one_of_roles', None):
is_authorized = list(set(kwargs.get('one_of_roles')) & set(roles)) != []
if kwargs.get('contains_role', None):
is_authorized = kwargs.get('contains_role') in roles
if jwt.validate_roles([Role.SYSTEM.value]):
is_authorized = bool(jwt.validate_roles([Role.EDITOR.value]))
else:
bearer_token = jwt.get_token_auth_header() if jwt else None
auth_url = current_app.config.get('AUTH_API_ENDPOINT') + f'entities/{business_identifier}/authorizations'
auth_response = RestService.get(auth_url, bearer_token, AuthHeaderType.BEARER, ContentType.JSON)

if auth_response:
roles: list = auth_response.json().get('roles', [])
if kwargs.get('one_of_roles', None):
is_authorized = list(set(kwargs.get('one_of_roles')) & set(roles)) != []
if kwargs.get('contains_role', None):
is_authorized = kwargs.get('contains_role') in roles

if not is_authorized:
abort(403)
7 changes: 3 additions & 4 deletions pay-api/src/pay_api/services/payment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,8 @@ def _complete_post_payment(pay_service: PaymentSystemService, payment: Payment):
{
'clientSystemUrl': '',
'payReturnUrl': ''
},
skip_auth_check=True)
transaction.update_transaction(payment.id, transaction.id, receipt_number=None, skip_auth_check=True)
})
transaction.update_transaction(payment.id, transaction.id, receipt_number=None)


def _update_active_transactions(payment_id):
Expand All @@ -375,7 +374,7 @@ def _update_active_transactions(payment_id):
current_app.logger.debug(transaction)
if transaction:
# check existing payment status in PayBC;
PaymentTransaction.update_transaction(payment_id, transaction.id, None, skip_auth_check=True)
PaymentTransaction.update_transaction(payment_id, transaction.id, None)


def _check_if_payment_is_completed(payment):
Expand Down
25 changes: 13 additions & 12 deletions pay-api/src/pay_api/services/payment_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from typing import Dict

from flask import current_app
from flask_jwt_oidc import JwtManager

from pay_api.exceptions import BusinessException, ServiceUnavailableException
from pay_api.factory.payment_system_factory import PaymentSystemFactory
Expand All @@ -29,9 +28,9 @@
from pay_api.services.invoice_reference import InvoiceReference
from pay_api.services.payment_account import PaymentAccount
from pay_api.services.receipt import Receipt
from pay_api.utils.constants import EDIT_ROLE
from pay_api.utils.enums import Status
from pay_api.utils.enums import PaymentSystem, Status
from pay_api.utils.errors import Error
from pay_api.utils.util import is_valid_redirect_url

from .invoice import InvoiceModel
from .payment import Payment
Expand Down Expand Up @@ -182,12 +181,16 @@ def flush(self):
return self._dao.flush()

@staticmethod
def create(payment_identifier: str, request_json: Dict, jwt: JwtManager = None, skip_auth_check: bool = False):
def create(payment_identifier: str, request_json: Dict):
"""Create transaction record."""
current_app.logger.debug('<create transaction')
# Lookup payment record
payment: Payment = Payment.find_by_id(payment_identifier, jwt=jwt, one_of_roles=[EDIT_ROLE],
skip_auth_check=skip_auth_check)
payment: Payment = Payment.find_by_id(payment_identifier, skip_auth_check=True)

# Check if return url is valid
return_url = request_json.get('clientSystemUrl')
if payment.payment_system_code == PaymentSystem.PAYBC.value and not is_valid_redirect_url(return_url):
raise BusinessException(Error.PAY013)

if not payment.id:
raise BusinessException(Error.PAY005)
Expand All @@ -206,7 +209,7 @@ def create(payment_identifier: str, request_json: Dict, jwt: JwtManager = None,

transaction = PaymentTransaction()
transaction.payment_id = payment.id
transaction.client_system_url = request_json.get('clientSystemUrl')
transaction.client_system_url = return_url
transaction.status_code = Status.CREATED.value
transaction_dao = transaction.flush()
transaction._dao = transaction_dao # pylint: disable=protected-access
Expand Down Expand Up @@ -262,8 +265,7 @@ def find_active_by_payment_id(payment_identifier: int):

@staticmethod
def update_transaction(payment_identifier: int, transaction_id: uuid, # pylint: disable=too-many-locals
receipt_number: str, jwt: JwtManager = None,
skip_auth_check: bool = False):
receipt_number: str):
"""Update transaction record.
Does the following:
Expand All @@ -283,8 +285,7 @@ def update_transaction(payment_identifier: int, transaction_id: uuid, # pylint:
if transaction_dao.status_code == Status.COMPLETED.value:
raise BusinessException(Error.PAY006)

payment: Payment = Payment.find_by_id(payment_identifier, jwt=jwt, one_of_roles=[EDIT_ROLE],
skip_auth_check=skip_auth_check)
payment: Payment = Payment.find_by_id(payment_identifier, skip_auth_check=True)

if payment.payment_status_code == Status.COMPLETED.value:
raise BusinessException(Error.PAY010)
Expand All @@ -293,7 +294,7 @@ def update_transaction(payment_identifier: int, transaction_id: uuid, # pylint:
payment_system=payment.payment_system_code
)

invoice = Invoice.find_by_payment_identifier(payment_identifier, jwt=jwt, skip_auth_check=True)
invoice = Invoice.find_by_payment_identifier(payment_identifier, skip_auth_check=True)
invoice_reference = InvoiceReference.find_active_reference_by_invoice_id(invoice.id)
payment_account = PaymentAccount.find_by_id(invoice.account_id)

Expand Down
3 changes: 1 addition & 2 deletions pay-api/src/pay_api/services/queue_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@
import random

from flask import current_app

from nats.aio.client import Client as NATS # noqa N814; by convention the name is NATS
from stan.aio.client import Client as STAN # noqa N814; by convention the name is STAN

from pay_api.utils.handlers import closed_cb, error_cb
from pay_api.utils.handlers import closed_cb, error_cb # noq I001; conflict with flake8


def publish_response(payload):
Expand Down
1 change: 1 addition & 0 deletions pay-api/src/pay_api/utils/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ class Role(Enum):
STAFF = 'staff'
VIEWER = 'view'
EDITOR = 'edit'
SYSTEM = 'system'
2 changes: 2 additions & 0 deletions pay-api/src/pay_api/utils/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class Error(Enum):
PAY010 = 'Payment is already completed', HTTPStatus.BAD_REQUEST
PAY011 = 'Payment is already cancelled', HTTPStatus.BAD_REQUEST
PAY012 = 'Invalid invoice identifier', HTTPStatus.BAD_REQUEST
PAY013 = 'Invalid redirect url', HTTPStatus.UNAUTHORIZED

PAY020 = 'Invalid Account Number for the User', HTTPStatus.BAD_REQUEST
PAY021 = 'Zero dollars deducted from BCOL', HTTPStatus.BAD_REQUEST

Expand Down
12 changes: 12 additions & 0 deletions pay-api/src/pay_api/utils/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
A simple decorator to add the options method to a Request Class.
"""
from flask import current_app


def cors_preflight(methods: str = 'GET'):
Expand All @@ -31,3 +32,14 @@ def options(self, *args, **kwargs): # pylint: disable=unused-argument
return f

return wrapper


def is_valid_redirect_url(url: str):
"""Validate if the url is valid based on the VALID Redirect Url."""
valid_urls: list = current_app.config.get('VALID_REDIRECT_URLS')
is_valid = False
for valid_url in valid_urls:
is_valid = url.startswith(valid_url[:-1]) if valid_url.endswith('*') else valid_url == url
if is_valid:
break
return is_valid
28 changes: 25 additions & 3 deletions pay-api/tests/unit/api/test_payment.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@

import json
from unittest.mock import patch
from copy import deepcopy

from requests.exceptions import ConnectionError

from pay_api.schemas import utils as schema_utils
from pay_api.utils.enums import Role
from tests.utilities.base_test import (
factory_payment_transaction, get_claims, get_payment_request, get_zero_dollar_payment_request, token_header)

Expand All @@ -41,6 +41,29 @@ def test_payment_creation(session, client, jwt, app):
assert schema_utils.validate(rv.json, 'payment_response')[0]


def test_payment_creation_with_service_account(session, client, jwt, app):
"""Assert that the endpoint returns 201."""
token = jwt.create_jwt(get_claims(roles=[Role.SYSTEM.value, Role.EDITOR.value]), token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}

rv = client.post(f'/api/v1/payment-requests', data=json.dumps(get_payment_request()),
headers=headers)
assert rv.status_code == 201
assert rv.json.get('_links') is not None

assert schema_utils.validate(rv.json, 'payment_response')[0]


def test_payment_creation_service_account_with_no_edit_role(session, client, jwt, app):
"""Assert that the endpoint returns 403."""
token = jwt.create_jwt(get_claims(role=Role.SYSTEM.value), token_header)
headers = {'Authorization': f'Bearer {token}', 'content-type': 'application/json'}

rv = client.post(f'/api/v1/payment-requests', data=json.dumps(get_payment_request()),
headers=headers)
assert rv.status_code == 403


def test_payment_creation_for_unauthorized_user(session, client, jwt, app):
"""Assert that the endpoint returns 403."""
token = jwt.create_jwt(get_claims(username='TEST', login_source='PASSCODE'), token_header)
Expand Down Expand Up @@ -360,8 +383,7 @@ def test_bcol_payment_creation(session, client, jwt, app):
}

rv = client.post(f'/api/v1/payment-requests', data=json.dumps(payload), headers=headers)
print(rv.json)
assert rv.status_code == 201
assert rv.json.get('_links') is not None

assert schema_utils.validate(rv.json, 'payment_response')[0]
assert schema_utils.validate(rv.json, 'payment_response')[0]
Loading

0 comments on commit 5f23be1

Please sign in to comment.