Skip to content

Commit

Permalink
Prmdr 481 (#176)
Browse files Browse the repository at this point in the history
* Moved business logic out of the token handler
* Improved error handling
  • Loading branch information
thisusernameisnowtaken authored Dec 8, 2023
1 parent 0f09b02 commit c2ea149
Show file tree
Hide file tree
Showing 53 changed files with 924 additions and 679 deletions.
3 changes: 1 addition & 2 deletions lambdas/handlers/bulk_upload_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
from utils.audit_logging_setup import LoggingService
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.exceptions import (InvalidMessageException,
PdsTooManyRequestsException)
from utils.exceptions import InvalidMessageException, PdsTooManyRequestsException
from utils.lloyd_george_validator import LGInvalidFilesException

logger = LoggingService(__name__)
Expand Down
3 changes: 1 addition & 2 deletions lambdas/handlers/create_document_reference_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from json import JSONDecodeError

from enums.logging_app_interaction import LoggingAppInteraction
from services.create_document_reference_service import \
CreateDocumentReferenceService
from services.create_document_reference_service import CreateDocumentReferenceService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.override_error_check import override_error_check
Expand Down
8 changes: 6 additions & 2 deletions lambdas/handlers/delete_document_reference_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.decorators.validate_document_type import (
extract_document_type_as_enum, validate_document_type)
extract_document_type_as_enum,
validate_document_type,
)
from utils.decorators.validate_patient_id import (
extract_nhs_number_from_event, validate_patient_id)
extract_nhs_number_from_event,
validate_patient_id,
)
from utils.lambda_response import ApiGatewayResponse
from utils.request_context import request_context

Expand Down
6 changes: 4 additions & 2 deletions lambdas/handlers/document_manifest_by_nhs_number_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.decorators.validate_document_type import (extract_document_type,
validate_document_type)
from utils.decorators.validate_document_type import (
extract_document_type,
validate_document_type,
)
from utils.decorators.validate_patient_id import validate_patient_id
from utils.exceptions import DocumentManifestServiceException
from utils.lambda_response import ApiGatewayResponse
Expand Down
7 changes: 4 additions & 3 deletions lambdas/handlers/document_reference_search_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@
from botocore.exceptions import ClientError
from enums.logging_app_interaction import LoggingAppInteraction
from enums.metadata_field_names import DocumentReferenceMetadataFields
from models.document_reference import (DocumentReference,
DocumentReferenceSearchResult)
from models.document_reference import DocumentReference, DocumentReferenceSearchResult
from pydantic import ValidationError
from services.document_service import DocumentService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.decorators.validate_patient_id import (
extract_nhs_number_from_event, validate_patient_id)
extract_nhs_number_from_event,
validate_patient_id,
)
from utils.exceptions import DynamoDbException, InvalidResourceIdException
from utils.lambda_response import ApiGatewayResponse
from utils.request_context import request_context
Expand Down
4 changes: 3 additions & 1 deletion lambdas/handlers/lloyd_george_record_stitch_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.decorators.validate_patient_id import (
extract_nhs_number_from_event, validate_patient_id)
extract_nhs_number_from_event,
validate_patient_id,
)
from utils.exceptions import DynamoDbException
from utils.lambda_response import ApiGatewayResponse
from utils.order_response_by_filenames import order_response_by_filenames
Expand Down
4 changes: 3 additions & 1 deletion lambdas/handlers/search_patient_details_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def lambda_handler(event, context):
if not user_role or not user_ods_code:
raise SearchPatientException(400, "Missing user details")

search_service = SearchPatientDetailsService(user_role=user_role, user_ods_code=user_ods_code)
search_service = SearchPatientDetailsService(
user_role=user_role, user_ods_code=user_ods_code
)
response = search_service.handle_search_patient_request(nhs_number)

return ApiGatewayResponse(200, response, "GET").create_api_gateway_response()
Expand Down
249 changes: 21 additions & 228 deletions lambdas/handlers/token_handler.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
import json
import os
import time
import uuid

import jwt
from boto3.dynamodb.conditions import Key
from botocore.exceptions import ClientError
from enums.logging_app_interaction import LoggingAppInteraction
from enums.repository_role import RepositoryRole
from models.oidc_models import IdTokenClaimSet
from oauthlib.oauth2 import WebApplicationClient
from services.dynamo_service import DynamoDBService
from services.ods_api_service import OdsApiService
from services.oidc_service import OidcService
from services.ssm_service import SSMService
from services.token_handler_ssm_service import TokenHandlerSSMService
from services.login_service import LoginService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.override_error_check import override_error_check
from utils.decorators.set_audit_arg import set_request_context_for_logging
from utils.exceptions import (AuthorisationException,
OrganisationNotFoundException,
TooManyOrgsException)
from utils.exceptions import LoginException
from utils.lambda_response import ApiGatewayResponse
from utils.request_context import request_context

logger = LoggingService(__name__)

token_handler_ssm_service = TokenHandlerSSMService()


@set_request_context_for_logging
@override_error_check
Expand All @@ -38,230 +21,40 @@
def lambda_handler(event, context):
request_context.app_interaction = LoggingAppInteraction.LOGIN.value

oidc_service = OidcService()
ods_api_service = OdsApiService()
missing_value_response_body = (
"No auth code and/or state in the query string parameters"
)

try:
oidc_service.set_up_oidc_parameters(SSMService, WebApplicationClient)
auth_code = event["queryStringParameters"]["code"]
state = event["queryStringParameters"]["state"]
if not (auth_code and state):
return response_400_bad_request_for_missing_parameter()
return respond_with(400, missing_value_response_body)
except (KeyError, TypeError):
return response_400_bad_request_for_missing_parameter()
return respond_with(400, missing_value_response_body)

try:
if not have_matching_state_value_in_record(state):
logger.info(
f"Mismatching state values. Cannot find state {state} in record"
)
return ApiGatewayResponse(
400,
"Failed to authenticate user",
"GET",
).create_api_gateway_response()

remove_used_state(state)

logger.info("Fetching access token from OIDC Provider")
access_token, id_token_claim_set = oidc_service.fetch_tokens(auth_code)

logger.info(
"Use the access token to fetch user's organisation and smartcard codes"
)
org_ods_codes = oidc_service.fetch_user_org_codes(
access_token, id_token_claim_set
)
smartcard_role_code, user_id = oidc_service.fetch_user_role_code(
access_token, id_token_claim_set, "R"
)
permitted_orgs_details = ods_api_service.fetch_organisation_with_permitted_role(
org_ods_codes
)

logger.info(f"Permitted_orgs_details: {permitted_orgs_details}")

if len(permitted_orgs_details.keys()) == 0:
logger.info("User has no org to log in with")
raise AuthorisationException(
f"{permitted_orgs_details.keys()} valid organisations for user"
)

session_id = create_login_session(id_token_claim_set)

logger.info("Calculating repository role")
repository_role = generate_repository_role(
permitted_orgs_details, smartcard_role_code
)

logger.info("Creating authorisation token")
authorisation_token = issue_auth_token(
session_id,
id_token_claim_set,
permitted_orgs_details,
smartcard_role_code,
repository_role.value,
user_id,
)

logger.info("Creating response")
response = {
"role": repository_role.value,
"authorisation_token": authorisation_token,
}
login_service = LoginService()

logger.audit_splunk_info(
"User logged in successfully", {"Result": "Successful login"}
)
return ApiGatewayResponse(
200, json.dumps(response), "GET"
).create_api_gateway_response()
session_info = login_service.generate_session(state, auth_code)

except AuthorisationException as error:
except LoginException as error:
logger.error(error, {"Result": "Unauthorised"})
return ApiGatewayResponse(
401, "Failed to authenticate user with OIDC service", "GET"
).create_api_gateway_response()
except (ClientError, KeyError, TypeError) as error:
logger.error(error, {"Result": "Unauthorised"})
return ApiGatewayResponse(
500, "Server error", "GET"
).create_api_gateway_response()
except jwt.PyJWTError as error:
logger.info(f"error while encoding JWT: {error}", {"Result": "Unauthorised"})
return ApiGatewayResponse(
500, "Server error", "GET"
).create_api_gateway_response()
except OrganisationNotFoundException as error:
logger.info(
f"Organisation does not exist for given ODS code: {error}",
{"Result": "Unauthorised"},
)
return ApiGatewayResponse(
500, "Organisation does not exist for given ODS code", "GET"
).create_api_gateway_response()
except TooManyOrgsException:
return ApiGatewayResponse(
500, "No single organisation found for given ods codes", "GET"
).create_api_gateway_response()


def generate_repository_role(organisation: dict, smartcart_role: str):
logger.info(f"Smartcard Role: {smartcart_role}")

if token_handler_ssm_service.get_smartcard_role_gp_admin() == smartcart_role:
logger.info("GP Admin: smartcard ODS identified")
if has_role_org_role_code(
organisation, token_handler_ssm_service.get_org_role_codes()[0]
):
return RepositoryRole.GP_ADMIN
return RepositoryRole.NONE

if token_handler_ssm_service.get_smartcard_role_gp_clinical() == smartcart_role:
logger.info("GP Clinical: smartcard ODS identified")
if has_role_org_role_code(
organisation, token_handler_ssm_service.get_org_role_codes()[0]
):
return RepositoryRole.GP_CLINICAL
return RepositoryRole.NONE

if token_handler_ssm_service.get_smartcard_role_pcse() == smartcart_role:
logger.info("PCSE: smartcard ODS identified")
if has_role_org_ods_code(
organisation, token_handler_ssm_service.get_org_ods_codes()[0]
):
return RepositoryRole.PCSE
return RepositoryRole.NONE

logger.info("Role: No smartcard role found")
return RepositoryRole.NONE


def has_role_org_role_code(organisation: dict, role_code: str) -> bool:
if organisation["role_code"].upper() == role_code.upper():
return True
return False

return respond_with(error.status_code, error.message)

def has_role_org_ods_code(organisation: dict, ods_code: str) -> bool:
if organisation["org_ods_code"].upper() == ods_code.upper():
return True
return False


# TODO AKH Dynamo Service class
def have_matching_state_value_in_record(state: str) -> bool:
state_table_name = os.environ["AUTH_STATE_TABLE_NAME"]

db_service = DynamoDBService()
query_response = db_service.simple_query(
table_name=state_table_name, key_condition_expression=Key("State").eq(state)
)

return "Count" in query_response and query_response["Count"] == 1


def remove_used_state(state):
state_table_name = os.environ["AUTH_STATE_TABLE_NAME"]
db_service = DynamoDBService()
deletion_key = {"State": state}
db_service.delete_item(table_name=state_table_name, key=deletion_key)


# TODO AKH Dynamo Service class
def create_login_session(id_token_claim_set: IdTokenClaimSet) -> str:
session_table_name = os.environ["AUTH_SESSION_TABLE_NAME"]

session_id = str(uuid.uuid4())
session_record = {
"NDRSessionId": session_id,
"sid": id_token_claim_set.sid,
"Subject": id_token_claim_set.sub,
"TimeToExist": id_token_claim_set.exp,
logger.info("Creating response")
response = {
"role": session_info["local_role"].value,
"authorisation_token": session_info["jwt"],
}

dynamodb_service = DynamoDBService()
dynamodb_service.create_item(table_name=session_table_name, item=session_record)

return session_id


# TODO AKH SSM service
def issue_auth_token(
session_id: str,
id_token_claim_set: IdTokenClaimSet,
user_org_details: list[dict],
smart_card_role: str,
repository_role: RepositoryRole,
user_id: str,
) -> str:
private_key = token_handler_ssm_service.get_jwt_private_key()

thirty_minutes_later = time.time() + (60 * 30)
ndr_token_expiry_time = min(thirty_minutes_later, id_token_claim_set.exp)

ndr_token_content = {
"exp": ndr_token_expiry_time,
"iss": "nhs repo",
"smart_card_role": smart_card_role,
"selected_organisation": user_org_details,
"repository_role": str(repository_role),
"ndr_session_id": session_id,
"nhs_user_id": user_id,
}

try:
authorisation_token = jwt.encode(
ndr_token_content, private_key, algorithm="RS256"
)
except Exception:
raise AuthorisationException("Unable to create authorisation token")

logger.info(f"encoded JWT: {authorisation_token}")
return authorisation_token
logger.audit_splunk_info(
"User logged in successfully", {"Result": "Successful login"}
)
return respond_with(200, json.dumps(response))


def response_400_bad_request_for_missing_parameter():
def respond_with(http_status_code, body):
return ApiGatewayResponse(
400, "Please supply an authorisation code and state", "GET"
http_status_code, body, "GET"
).create_api_gateway_response()
4 changes: 2 additions & 2 deletions lambdas/services/authoriser_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ def find_login_session(self, ndr_session_id):
return current_session
except (KeyError, IndexError):
raise AuthorisationException(
f"Unable to find session for session ID ending in: {self.redact_session_id}"
f"Unable to find session for session ID ending in: {self.redact_session_id}",
)

def validate_login_session(self, session_expiry_time: float):
time_now = time.time()
if session_expiry_time <= time_now:
raise AuthorisationException(
f"The session is already expired for session ID ending in: {self.redact_session_id}"
f"The session is already expired for session ID ending in: {self.redact_session_id}",
)
Loading

0 comments on commit c2ea149

Please sign in to comment.