Skip to content

Commit

Permalink
17443 Add auth to colin api (#2187)
Browse files Browse the repository at this point in the history
* 17443 Add auth to colin api

* Fix lint errors

* Revert legal api updates
  • Loading branch information
argush3 authored Sep 6, 2023
1 parent d90f11a commit ac90e01
Show file tree
Hide file tree
Showing 19 changed files with 258 additions and 33 deletions.
4 changes: 3 additions & 1 deletion colin-api/devops/vaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"application": [
"colin-api",
"test-oracle",
"sentry"
"sentry",
"jwt",
"launchdarkly"
]
}
]
5 changes: 5 additions & 0 deletions colin-api/flags.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"flagValues": {
"disable-colin-api-auth": true
}
}
26 changes: 15 additions & 11 deletions colin-api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
Flask-Moment==0.10.0

Flask-Moment==0.11.0
Flask-Script==2.0.6
Flask==1.1.2
Jinja2==2.11.2
Jinja2==2.11.3
MarkupSafe==1.1.1
Werkzeug==1.0.1
aniso8601==8.1.0
attrs==20.3.0
aniso8601==9.0.1
blinker==1.4
certifi==2020.12.5
click==7.1.2
click==8.1.3
cx-Oracle==8.1.0
debugpy
ecdsa==0.14.1
flask-jwt-oidc==0.1.5
flask-jwt-oidc==0.3.0
flask-restx==0.3.0
gunicorn==20.0.4
gunicorn==20.1.0
itsdangerous==1.1.0
jsonschema==3.2.0
jsonschema==4.19.0
launchdarkly-server-sdk==7.1.0
psycopg2-binary==2.8.6
pyasn1==0.4.8
pycountry==20.7.3
pyrsistent==0.17.3
python-dotenv==0.15.0
python-dotenv==0.17.1
python-jose==3.2.0
pytz==2020.4
rsa==4.6
pytz==2021.1
requests==2.25.1
rsa==4.7.2
SQLAlchemy==1.4.44
sentry-sdk==1.20.0
six==1.15.0
urllib3==1.26.11
git+https://github.com/bcgov/lear.git#egg=legal_api&subdirectory=legal-api
git+https://github.com/bcgov/business-schemas.git#egg=registry_schemas
4 changes: 2 additions & 2 deletions colin-api/requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pytest-mock
pytest-cov
requests
pyhamcrest
sqlalchemy
sqlalchemy<=1.4.44

# Lint and code style
flake8
Expand All @@ -22,4 +22,4 @@ pydocstyle<4.0
pylint<=2.3.1
pylint-flask
isort<5,>=4.2.5
sqlalchemy
sqlalchemy<=1.4.44
17 changes: 9 additions & 8 deletions colin-api/src/colin_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,21 @@
This module is the API for the Legal Entity system.
"""
import os

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

from flask import Flask
from flask_jwt_oidc import JwtManager
from legal_api.services import flags
from sentry_sdk.integrations.flask import FlaskIntegration # noqa: I001

from colin_api import config
from colin_api import config, errorhandlers
from colin_api.resources import API_BLUEPRINT, OPS_BLUEPRINT
from colin_api.utils.auth import jwt
from colin_api.utils.logging import setup_logging
from colin_api.utils.run_version import get_run_version
# noqa: I003; the sentry import creates a bad line count in isort

setup_logging(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'logging.conf')) # important to do this first

# lower case name as used by convention in most Flask apps
jwt = JwtManager() # pylint: disable=invalid-name


def create_app(run_mode=os.getenv('FLASK_ENV', 'production')):
"""Return a configured Flask App using the Factory method."""
Expand All @@ -44,9 +42,12 @@ def create_app(run_mode=os.getenv('FLASK_ENV', 'production')):
dsn=app.config.get('SENTRY_DSN'),
integrations=[FlaskIntegration()]
)

flags.init_app(app)
errorhandlers.init_app(app)
app.register_blueprint(API_BLUEPRINT)
app.register_blueprint(OPS_BLUEPRINT)
# setup_jwt_manager(app, jwt)
setup_jwt_manager(app, jwt)

@app.after_request
def add_version(response): # pylint: disable=unused-variable
Expand Down
20 changes: 20 additions & 0 deletions colin-api/src/colin_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class _Config: # pylint: disable=too-few-public-methods

SENTRY_DSN = os.getenv('SENTRY_DSN', '')

LD_SDK_KEY = os.getenv('LD_SDK_KEY', None)

# ORACLE - CDEV/CTST/CPRD
ORACLE_USER = os.getenv('ORACLE_USER', '')
ORACLE_PASSWORD = os.getenv('ORACLE_PASSWORD', '')
Expand All @@ -72,6 +74,24 @@ class _Config: # pylint: disable=too-few-public-methods
ORACLE_PORT = int(os.getenv('ORACLE_PORT', '1521'))
ORACLE_BNI_DB_LINK = os.getenv('ORACLE_BNI_DB_LINK', '')

# JWT_OIDC Settings
JWT_OIDC_WELL_KNOWN_CONFIG = os.getenv('JWT_OIDC_WELL_KNOWN_CONFIG')
JWT_OIDC_ALGORITHMS = os.getenv('JWT_OIDC_ALGORITHMS')
JWT_OIDC_JWKS_URI = os.getenv('JWT_OIDC_JWKS_URI')
JWT_OIDC_ISSUER = os.getenv('JWT_OIDC_ISSUER')
JWT_OIDC_AUDIENCE = os.getenv('JWT_OIDC_AUDIENCE')
JWT_OIDC_CLIENT_SECRET = os.getenv('JWT_OIDC_CLIENT_SECRET')
JWT_OIDC_CACHING_ENABLED = os.getenv('JWT_OIDC_CACHING_ENABLED')
JWT_OIDC_USERNAME = os.getenv('JWT_OIDC_USERNAME', 'username')
JWT_OIDC_FIRSTNAME = os.getenv('JWT_OIDC_FIRSTNAME', 'firstname')
JWT_OIDC_LASTNAME = os.getenv('JWT_OIDC_LASTNAME', 'lastname')
try:
JWT_OIDC_JWKS_CACHE_TIMEOUT = int(os.getenv('JWT_OIDC_JWKS_CACHE_TIMEOUT'))
if not JWT_OIDC_JWKS_CACHE_TIMEOUT:
JWT_OIDC_JWKS_CACHE_TIMEOUT = 300
except (TypeError, ValueError):
JWT_OIDC_JWKS_CACHE_TIMEOUT = 300

TESTING = False
DEBUG = False

Expand Down
64 changes: 64 additions & 0 deletions colin-api/src/colin_api/errorhandlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright © 2023 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.

"""Core error handlers and custom exceptions.
Following best practices from:
http://flask.pocoo.org/docs/1.0/errorhandling/
http://flask.pocoo.org/docs/1.0/patterns/apierrors/
"""

import logging
import sys

from flask import jsonify
from werkzeug.exceptions import HTTPException
from werkzeug.routing import RoutingException


logger = logging.getLogger(__name__)


def init_app(app):
"""Initialize the error handlers for the Flask app instance."""
app.register_error_handler(HTTPException, handle_http_error)
app.register_error_handler(Exception, handle_uncaught_error)


def handle_http_error(error):
"""Handle HTTPExceptions.
Include the error description and corresponding status code, known to be
available on the werkzeug HTTPExceptions.
"""
# As werkzeug's routing exceptions also inherit from HTTPException,
# check for those and allow them to return with redirect responses.
if isinstance(error, RoutingException):
return error

response = jsonify({'message': error.description})
response.status_code = error.code
return response


def handle_uncaught_error(error: Exception): # pylint: disable=unused-argument
"""Handle any uncaught exceptions.
Since the handler suppresses the actual exception, log it explicitly to
ensure it's logged and recorded in Sentry.
"""
logger.error('Uncaught exception', exc_info=sys.exc_info())
response = jsonify({'message': 'Internal server errors'})
response.status_code = 500
return response
7 changes: 6 additions & 1 deletion colin-api/src/colin_api/resources/business.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from colin_api.exceptions import GenericException
from colin_api.models import Business, CorpName
from colin_api.resources.db import DB
from colin_api.utils.util import cors_preflight
from colin_api.utils.auth import COLIN_SVC_ROLE, jwt
from colin_api.utils.util import conditional_auth, cors_preflight


API = Namespace('businesses', description='Colin API Services - Businesses')
Expand All @@ -37,6 +38,7 @@ class BusinessInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(legal_type: str, identifier: str):
"""Return the complete business info."""
try:
Expand All @@ -63,6 +65,7 @@ def get(legal_type: str, identifier: str):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def post(legal_type: str):
"""Create and return a new corp number for the given legal type."""
if legal_type not in [x.value for x in Business.LearBusinessTypes]:
Expand Down Expand Up @@ -91,6 +94,7 @@ class BusinessNamesInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(legal_type, identifier, name_type):
"""Get active names by type code for a business."""
if legal_type not in [x.value for x in Business.LearBusinessTypes]:
Expand Down Expand Up @@ -124,6 +128,7 @@ class InternalBusinessInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(info_type, legal_type=None, identifier=None): # pylint: disable = too-many-return-statements;
"""Return specific business info for businesses."""
try:
Expand Down
5 changes: 4 additions & 1 deletion colin-api/src/colin_api/resources/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

from colin_api.resources.business import API
from colin_api.resources.db import DB
from colin_api.utils.util import cors_preflight
from colin_api.utils.auth import COLIN_SVC_ROLE, jwt
from colin_api.utils.util import conditional_auth, cors_preflight


@cors_preflight('GET, POST')
Expand All @@ -27,6 +28,7 @@ class EventInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(corp_type, event_id):
"""Return all event_ids of the corp_type that are greater than the given event_id."""
querystring = ("""
Expand Down Expand Up @@ -64,6 +66,7 @@ class CorpEventInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(corp_num):
"""Return all event_ids of the corp_type that are greater than the given event_id."""
querystring = ("""
Expand Down
7 changes: 4 additions & 3 deletions colin-api/src/colin_api/resources/filing.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@

from flask import current_app, jsonify, request
from flask_restx import Resource, cors
# from registry_schemas import validate # noqa: I001

from colin_api.exceptions import GenericException
from colin_api.models import Business
from colin_api.models.filing import DB, Filing
from colin_api.resources.business import API
from colin_api.utils import convert_to_pacific_time
from colin_api.utils.util import cors_preflight
# noqa: I003
from colin_api.utils.auth import COLIN_SVC_ROLE, jwt
from colin_api.utils.util import conditional_auth, cors_preflight


@cors_preflight('GET, POST')
Expand All @@ -38,6 +37,7 @@ class FilingInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(legal_type, identifier, filing_type, filing_sub_type=None):
"""Return the complete filing info or historic (pre-bob-date=2019-03-08) filings."""
try:
Expand Down Expand Up @@ -86,6 +86,7 @@ def get(legal_type, identifier, filing_type, filing_sub_type=None):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def post(legal_type, identifier, **kwargs):
"""Create a new filing."""
# pylint: disable=unused-argument,too-many-branches; filing_type is only used for the get
Expand Down
4 changes: 3 additions & 1 deletion colin-api/src/colin_api/resources/office.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from colin_api.models import Business, Office
from colin_api.models.filing import DB
from colin_api.resources.business import API
from colin_api.utils.util import cors_preflight
from colin_api.utils.auth import COLIN_SVC_ROLE, jwt
from colin_api.utils.util import conditional_auth, cors_preflight


@cors_preflight('GET')
Expand All @@ -34,6 +35,7 @@ class OfficeInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(legal_type: str, identifier: str):
"""Return the registered and/or records office for a corporation."""
if not identifier:
Expand Down
5 changes: 4 additions & 1 deletion colin-api/src/colin_api/resources/parties.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
from colin_api.models import Business, Party
from colin_api.models.filing import DB
from colin_api.resources.business import API
from colin_api.utils.util import cors_preflight
from colin_api.utils.auth import COLIN_SVC_ROLE, jwt
from colin_api.utils.util import conditional_auth, cors_preflight


@cors_preflight('GET')
Expand All @@ -34,6 +35,7 @@ class PartiesInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(legal_type: str, identifier: str):
"""Return the current directors for a business."""
if not identifier:
Expand Down Expand Up @@ -69,6 +71,7 @@ class Parties(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(legal_type: str, identifier: str):
"""Return all the parties for a business."""
try:
Expand Down
4 changes: 3 additions & 1 deletion colin-api/src/colin_api/resources/program_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@

from colin_api.exceptions import GenericException
from colin_api.models import ProgramAccount
from colin_api.utils.util import cors_preflight
from colin_api.utils.auth import COLIN_SVC_ROLE, jwt
from colin_api.utils.util import conditional_auth, cors_preflight


API = Namespace('ProgramAccount', description='ProgramAccount endpoint to get BNI DB link data.')
Expand All @@ -33,6 +34,7 @@ class ProgramAccountInfo(Resource):

@staticmethod
@cors.crossdomain(origin='*')
@conditional_auth(jwt.requires_roles, [COLIN_SVC_ROLE])
def get(identifier: str, transaction_id: str = None):
"""Return the BNI DB link program account."""
if not identifier:
Expand Down
Loading

0 comments on commit ac90e01

Please sign in to comment.