Skip to content

Commit

Permalink
23504 - Endpoint for cc confirmation.
Browse files Browse the repository at this point in the history
  • Loading branch information
hfekete committed Oct 7, 2024
1 parent 218f33d commit eb09ebf
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 5 deletions.
4 changes: 4 additions & 0 deletions search-api/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ JWT_OIDC_WELL_KNOWN_CONFIG=
ACCOUNT_SVC_AUTH_URL=
ACCOUNT_SVC_CLIENT_ID=
ACCOUNT_SVC_CLIENT_SECRET=

#payment queue stuff
PUBLISHER_AUDIENCE="op://gcp-queue/$APP_ENV/base/PUBLISHER_AUDIENCE"
PAY_AUDIENCE_SUB="op://gcp-queue/$APP_ENV/base/PAY_AUDIENCE_SUB"
6 changes: 4 additions & 2 deletions search-api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@ six==1.16.0
strict-rfc3339==0.7
typing_extensions==4.12.2
urllib3==1.26.16
git+https://github.com/daxiom/simple-cloudevent.py.git@0.0.2
git+https://github.com/daxiom/flask-pub.git@0.0.4
cachecontrol==0.14.0
git+https://github.com/daxiom/simple-cloudevent.py.git
git+https://github.com/daxiom/flask-pub.git@0.0.4
git+https://github.com/bcgov/sbc-connect-common.git@43411ed428c4c4b89bea1ac6acdb10077f247d2b#egg=gcp_queue&subdirectory=python\gcp-queue
5 changes: 5 additions & 0 deletions search-api/src/search_api/models/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class DbRowNotFound(Exception):
"""Row not found in database"""
def __init__(self):
self.message = "not.found.in.db"
super().__init__(self.message)
4 changes: 2 additions & 2 deletions search-api/src/search_api/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"""Exposes the versioned endpoints."""
from .constants import EndpointVersionPath
from .v1 import bus_bp, internal_bp, meta_bp, ops_bp, purchases_bp
from .v2 import search_bp
from .v2 import search_bp, payments_bp
from .version_endpoint import VersionEndpoint


Expand All @@ -26,4 +26,4 @@
v2_endpoint = VersionEndpoint( # pylint: disable=invalid-name
name='API_V2',
path=EndpointVersionPath.API_V2,
bps=[search_bp])
bps=[search_bp, payments_bp])
1 change: 1 addition & 0 deletions search-api/src/search_api/resources/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
# limitations under the License.
"""Exposes all of the resource v2 endpoints in Flask-Blueprint style."""
from .search import bp as search_bp
from .payments import bp as payments_bp
20 changes: 20 additions & 0 deletions search-api/src/search_api/resources/v2/payments/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright © 2024 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""API endpoints for Search."""
from flask import Blueprint

from .payments import bp as payments_bp

bp = Blueprint('PAYMENTS', __name__, url_prefix='/payments') # pylint: disable=invalid-name
bp.register_blueprint(payments_bp)
54 changes: 54 additions & 0 deletions search-api/src/search_api/resources/v2/payments/payments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright © 2024 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""API endpoints for Search Credit Card (CC) payments."""
import dataclasses
import json
from http import HTTPStatus

from flask import Blueprint, current_app, request
from flask_cors import cross_origin

from search_api.services import simple_queue
from search_api.services.document_services.document_access_request import update_document_access_request_status_by_id
from search_api.services.gcp_auth.auth_service import ensure_authorized_queue_user

bp = Blueprint('GCP_LISTENER', __name__) # pylint: disable=invalid-name



@bp.route("/", methods=("POST",))
@cross_origin(origin='*')
@ensure_authorized_queue_user
def gcp_listener():
"""Process the incoming cloud event.
returns status
200 - on success or invalid message (do not return invalid message in the queue)
500 - if any issues returns 500 Internal Server Error so msg can return to the queue
"""
ce = simple_queue.get_simple_cloud_event(request, wrapped=True)

if not ce or not ce.data or not ce.data.id or not ce.data.status:
current_app.logger.error('Invalid Event Message Received: %s ', json.dumps(dataclasses.asdict(ce)))
return {}, HTTPStatus.BAD_REQUEST

try:
credit_card_payment = ce.data
update_document_access_request_status_by_id(credit_card_payment.id, credit_card_payment.status)

return {}, HTTPStatus.OK
except Exception: # NOQA # pylint:Q disable=broad-except
# Catch Exception so that any error is still caught and the message is removed from the queue
current_app.logger.error('Error processing event: ', exc_info=True)
return {}, HTTPStatus.INTERNAL_SERVER_ERROR
2 changes: 2 additions & 0 deletions search-api/src/search_api/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .business_solr import BusinessSolr
from .flags import Flags
from .queue import Queue
from gcp_queue import GcpQueue


flags = Flags() # pylint: disable=invalid-name; shared variables are lower case by Flask convention.
Expand All @@ -25,3 +26,4 @@
queue = Queue() # pylint: disable=invalid-name; shared variables are lower case by Flask convention.
# TODO: uncomment after testing with running gcp service
# storage = GoogleStorageService() # pylint: disable=invalid-name; shared variables are lower case by Flask convention.
simple_queue = GcpQueue()
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright © 2022 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Handles the Document Access Request updates."""
from search_api.models import DocumentAccessRequest
from search_api.models.errors import DbRowNotFound


def update_document_access_request_status_by_id(dar_id: int, status: str):
dar = DocumentAccessRequest.find_by_id(dar_id)

if not dar:
raise DbRowNotFound()

dar.status = status
dar.save()
40 changes: 39 additions & 1 deletion search-api/src/search_api/services/gcp_auth/auth_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
# limitations under the License.
"""This maintains access tokens for API calls."""
import base64
import functools
import json
import os

import google.auth.transport.requests
import google.oauth2.id_token as id_token

from cachecontrol import CacheControl
from flask import abort, current_app, request
from google.oauth2 import service_account
from flask import current_app
from http import HTTPStatus
from requests.sessions import Session

from search_api.services.gcp_auth.abstract_auth_service import AuthService

Expand Down Expand Up @@ -61,3 +67,35 @@ def get_credentials(cls):
scopes=cls.GCP_SA_SCOPES)
current_app.logger.info('Call successful: obtained credentials.')
return cls.credentials


def verify_jwt(session):
"""Check token is valid with the correct audience and email claims for configured email address."""
try:
jwt_token = request.headers.get('Authorization', '').split()[1]
claims = id_token.verify_oauth2_token(
jwt_token,
google.auth.transport.requests.Request(session=session),
audience=current_app.config.get('PAY_AUDIENCE_SUB')
)
required_emails = current_app.config.get('VERIFY_PUBSUB_EMAILS')
if claims.get('email_verified') and claims.get('email') in required_emails:
return None
else:
return 'Email not verified or does not match', 401
except Exception as e:
current_app.logger.info(f'Invalid token {e}')
return f'Invalid token: {e}', 400


def ensure_authorized_queue_user(f):
"""Ensures the user is authorized to use the queue."""

@functools.wraps(f)
def decorated_function(*args, **kwargs):
# Use CacheControl to avoid re-fetching certificates for every request.
if verify_jwt(CacheControl(Session())):
abort(HTTPStatus.UNAUTHORIZED)
return f(*args, **kwargs)

return decorated_function

0 comments on commit eb09ebf

Please sign in to comment.