Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions compliance-api/src/compliance_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
from http import HTTPStatus

import secure
from flask import Flask, current_app, g, request
from flask import Flask, current_app, g, jsonify, request
from flask_cors import CORS
from jose import jwt as jose_jwt
from werkzeug.exceptions import HTTPException

from compliance_api.auth import jwt
from compliance_api.config import get_named_config
Expand Down Expand Up @@ -121,13 +122,29 @@ def shutdown_session(exception=None): # pylint: disable=unused-argument
"""Execute teardown actions."""
db.session.remove()

@app.errorhandler(HTTPException)
def handle_http_exception(error):
"""Handle all HTTP exceptions with proper JSON responses."""
response = {
"message": error.description or str(error),
"status": error.code
}
return jsonify(response), error.code

@app.errorhandler(Exception)
def handle_error(err):
"""Handle all other non-HTTP exceptions."""
if run_mode != "production":
# To get stacktrace in local development for internal server errors
# In development, re-raise to see full stacktrace
raise err
current_app.logger.error(str(err))
return "Internal server error", HTTPStatus.INTERNAL_SERVER_ERROR

current_app.logger.error(f"Unhandled exception: {str(err)}", exc_info=True)

response = {
"message": "An internal server error occurred",
"status": 500
}
return jsonify(response), HTTPStatus.INTERNAL_SERVER_ERROR

# Return App for run in run.py file
return app
Expand Down
12 changes: 11 additions & 1 deletion compliance-api/src/compliance_api/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
error - a description of the error {code / description: classname / full text}
status_code - where possible use HTTP Error Codes
"""
from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound, UnprocessableEntity
from werkzeug.exceptions import BadRequest, Conflict, Forbidden, NotFound, ServiceUnavailable, UnprocessableEntity
from werkzeug.wrappers.response import Response


Expand Down Expand Up @@ -81,3 +81,13 @@ def __init__(self, message, *args, **kwargs):
super().__init__(*args, **kwargs)
self.description = message
self.response = Response(message, status=UnprocessableEntity.code)


class ServiceUnavailableError(ServiceUnavailable):
"""Exception raised when an external service is unavailable."""

def __init__(self, message, *args, **kwargs):
"""Return a valid ServiceUnavailableError."""
super().__init__(*args, **kwargs)
self.description = message
self.response = Response(message, status=ServiceUnavailable.code)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import requests
from flask import current_app, g

from compliance_api.exceptions import BusinessError
from compliance_api.exceptions import BadRequestError, BusinessError, ResourceNotFoundError, ServiceUnavailableError
from compliance_api.utils.constant import AUTH_APP
from compliance_api.utils.enum import HttpMethod

Expand All @@ -16,45 +16,140 @@ class AuthService:
@staticmethod
def get_epic_user_by_guid(auth_user_guid: str):
"""Return the user representation from epic.authorize."""
auth_user_response = _request_auth_service(f"users/{auth_user_guid}")
if auth_user_response.status_code != 200:
raise BusinessError(
f"Error finding user with ID {auth_user_guid} from auth server"
try:
response = _request_auth_service(f"users/{auth_user_guid}")

if response.status_code == 404:
raise ResourceNotFoundError(
f"User with ID {auth_user_guid} not found in EPIC.auth"
)

if response.status_code != 200:
current_app.logger.error(
f"EPIC.auth returned status {response.status_code} "
f"for user {auth_user_guid}"
)
raise BadRequestError(
"Unable to retrieve user information at this time"
)

return response.json()

except requests.exceptions.RequestException as e:
current_app.logger.error(
f"EPIC.auth service unavailable for user {auth_user_guid}: {str(e)}",
exc_info=True,
)
raise ServiceUnavailableError(
"The user service is temporarily unavailable. Please try again later."
)

except (ResourceNotFoundError, BadRequestError):
raise

except (KeyError, ValueError, TypeError) as e:
current_app.logger.error(
f"Error parsing user data for {auth_user_guid}: {str(e)}",
exc_info=True,
)
raise BadRequestError(
f"Unable to parse user information for {auth_user_guid}"
)
return auth_user_response.json()

@staticmethod
def get_epic_users_by_app():
"""Return all users belonging to COMPLIANCE app."""
auth_users_response = _request_auth_service(f"users?app_name={AUTH_APP}")
if auth_users_response.status_code != 200:
raise BusinessError(f"Error fetching users for the app {AUTH_APP}")
return auth_users_response.json()
try:
response = _request_auth_service(f"users?app_name={AUTH_APP}")

if response.status_code != 200:
current_app.logger.error(
f"EPIC.auth returned status {response.status_code} for app users"
)
raise BadRequestError(
f"Unable to retrieve users for the app {AUTH_APP}"
)

return response.json()

except requests.exceptions.RequestException as e:
current_app.logger.error(
f"EPIC.auth service unavailable for app users: {str(e)}",
exc_info=True,
)
raise ServiceUnavailableError(
"The user service is temporarily unavailable. Please try again later."
)

except BadRequestError:
raise

except (KeyError, ValueError, TypeError) as e:
current_app.logger.error(
f"Error parsing app users response: {str(e)}",
exc_info=True,
)
raise BadRequestError("Unable to parse user information")

@staticmethod
def update_user_group(auth_user_guid: str, payload: dict):
"""Update the group of the user in the identity server."""
update_group_response = _request_auth_service(
f"users/{auth_user_guid}/groups", HttpMethod.PUT, payload
)
if update_group_response.status_code != 204:
raise BusinessError(
f"Update group in the auth server failed for user : {auth_user_guid}"
try:
response = _request_auth_service(
f"users/{auth_user_guid}/groups",
HttpMethod.PUT,
payload,
)

return update_group_response
if response.status_code != 204:
current_app.logger.error(
f"EPIC.auth returned status {response.status_code} "
f"for updating user group {auth_user_guid}"
)
raise BadRequestError(
f"Update group failed for user {auth_user_guid}"
)

return response

except requests.exceptions.RequestException as e:
current_app.logger.error(
f"EPIC.auth service unavailable while updating group "
f"for {auth_user_guid}: {str(e)}",
exc_info=True,
)
raise ServiceUnavailableError(
"The user service is temporarily unavailable. Please try again later."
)

@staticmethod
def delete_user_group(auth_user_guid: str, group: str, del_sub_group_mappings=True):
"""Delete user group."""
delete_response = _request_auth_service(
f"users/{auth_user_guid}/groups/{group}?del_sub_group_mappings={del_sub_group_mappings}",
HttpMethod.DELETE,
)
if delete_response.status_code != 204:
raise BusinessError("Delete group mapping failed")
try:
response = _request_auth_service(
f"users/{auth_user_guid}/groups/{group}"
f"?del_sub_group_mappings={del_sub_group_mappings}",
HttpMethod.DELETE,
)

if response.status_code != 204:
current_app.logger.error(
f"EPIC.auth returned status {response.status_code} "
f"for deleting group {group} for user {auth_user_guid}"
)
raise BadRequestError("Delete group mapping failed")

return delete_response
return response

except requests.exceptions.RequestException as e:
current_app.logger.error(
f"EPIC.auth service unavailable while deleting group "
f"for {auth_user_guid}: {str(e)}",
exc_info=True,
)
raise ServiceUnavailableError(
"The user service is temporarily unavailable. Please try again later."
)


def _request_auth_service(
Expand All @@ -73,19 +168,24 @@ def _request_auth_service(

url = f"{auth_base_url}/api/{relative_url}"

if http_method == HttpMethod.GET:
response = requests.get(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
elif http_method == HttpMethod.PUT:
response = requests.put(
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
)
elif http_method == HttpMethod.PATCH:
response = requests.patch(
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
)
elif http_method == HttpMethod.DELETE:
response = requests.delete(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
else:
raise ValueError("Invalid HTTP method")
response.raise_for_status()
try:
if http_method == HttpMethod.GET:
response = requests.get(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
elif http_method == HttpMethod.PUT:
response = requests.put(
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
)
elif http_method == HttpMethod.PATCH:
response = requests.patch(
url, headers=headers, json=data, timeout=API_REQUEST_TIMEOUT
)
elif http_method == HttpMethod.DELETE:
response = requests.delete(url, headers=headers, timeout=API_REQUEST_TIMEOUT)
else:
raise ValueError("Invalid HTTP method")
response.raise_for_status()
except requests.exceptions.RequestException as e:
raise requests.exceptions.RequestException(
f"Error making request to EPIC.track server: {str(e)}"
) from e
return response
4 changes: 4 additions & 0 deletions compliance-api/src/compliance_api/services/case_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,10 @@ def _set_project_parameters(case_file):
setattr(case_file, "regulated_party", project.get("proponent").get("name"))
if not project_id:
project = UnapprovedProjectModel.get_by_case_file_id(case_file.id)
if not project:
raise ResourceNotFoundError(
f"No project information found for case file {case_file.id}"
)
setattr(case_file, "authorization", project.authorization)
setattr(case_file, "type", project.type)
setattr(case_file, "sub_type", project.sub_type)
Expand Down
Loading