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
14 changes: 14 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -940,3 +940,17 @@ jobs:
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_create_user_restriction_lambda:
name: Deploy Create User Restriction Lambda
uses: ./.github/workflows/base-lambdas-reusable-deploy.yml
with:
environment: ${{ inputs.environment }}
python_version: ${{ inputs.python_version }}
build_branch: ${{ inputs.build_branch }}
sandbox: ${{ inputs.sandbox }}
lambda_handler_path: user_restrictions
lambda_handler_name: create_user_restriction_handler
lambda_aws_name: CreateUserRestriction
lambda_layer_names: "core_lambda_layer"
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}
53 changes: 51 additions & 2 deletions lambdas/enums/lambda_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class ErrorMessage(StrEnum):
FAILED_TO_VALIDATE = "Failed to validate data"
FAILED_TO_UPDATE_DYNAMO = "Failed to update DynamoDB"
FAILED_TO_CREATE_TRANSACTION = "Failed to create transaction"
INVALID_REQUEST_BODY = "Invalid request body"


class LambdaError(Enum):
Expand Down Expand Up @@ -503,7 +504,7 @@ def create_error_body(
}
UpdateUploadStateInvalidBody = {
"err_code": "US_4005",
"message": "Invalid request body",
"message": ErrorMessage.INVALID_REQUEST_BODY,
}
UpdateUploadStateFieldType = {
"err_code": "US_4006",
Expand Down Expand Up @@ -565,7 +566,7 @@ def create_error_body(

DocumentReviewInvalidBody = {
"err_code": "DRV_4004",
"message": "Invalid request body",
"message": ErrorMessage.INVALID_REQUEST_BODY,
}

DocumentReviewInvalidNhsNumber = {
Expand Down Expand Up @@ -755,6 +756,54 @@ def create_error_body(
"message": "Failed to parse SQS event",
}

"""
Errors for UserRestriction lambda
"""
CreateRestrictionMissingBody = {
"err_code": "UR_4001",
"message": "Missing request body",
}
CreateRestrictionMissingFields = {
"err_code": "UR_4002",
"message": "Missing required fields",
}
CreateRestrictionPatientIdMismatch = {
"err_code": "UR_4003",
"message": "patientId does not match nhs_number",
}
CreateRestrictionMissingContext = {
"err_code": "UR_4004",
"message": "Missing user context",
}
CreateRestrictionInvalidWorker = {
"err_code": "UR_4005",
"message": "Invalid Worker",
}
CreateRestrictionPractitionerModelError = {
"err_code": "UR_4006",
"message": "Unable to process restricted user information",
}
CreateRestrictionSelfRestriction = {
"err_code": "UR_4007",
"message": "You cannot create a restriction for yourself",
}
CreateRestrictionAlreadyExists = {
"err_code": "UR_4009",
"message": "A restriction already exists for this user and patient",
}
CreateRestrictionInvalidBody = {
"err_code": "UR_4008",
"message": ErrorMessage.INVALID_REQUEST_BODY,
}
CreateRestrictionPatientOdsMismatch = {
"err_code": "UR_4010",
"message": "Patient's general practice ODS does not match request context ODS",
}
CreateRestrictionPatientNotFound = {
"err_code": "UR_4011",
"message": "Patient not found in PDS",
}

MockError = {
"message": "Client error",
"err_code": "AB_XXXX",
Expand Down
1 change: 1 addition & 0 deletions lambdas/enums/logging_app_interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ class LoggingAppInteraction(Enum):
MANIFEST_JOB = "Manifest job"
RESTRICTION_SOFT_DELETE = "User restrictions - soft-delete"
SEARCH_HISTORY = "Search document reference history"
USER_RESTRICTION = "User restriction"
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import json

from enums.feature_flags import FeatureFlags
from enums.lambda_error import LambdaError
from enums.logging_app_interaction import LoggingAppInteraction
from services.feature_flags_service import FeatureFlagService
from services.user_restrictions.create_user_restriction_service import (
CreateUserRestrictionService,
)
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions
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,
)
from utils.exceptions import (
HealthcareWorkerAPIException,
HealthcareWorkerPractitionerModelException,
OdsErrorException,
UserRestrictionAlreadyExistsException,
)
from utils.lambda_exceptions import LambdaException
from utils.lambda_response import ApiGatewayResponse
from utils.ods_utils import extract_creator_and_ods_code_from_request_context
from utils.request_context import request_context

logger = LoggingService(__name__)


def parse_body(body: str | None) -> tuple[str, str]:
if not body:
logger.error("Missing request body")
raise LambdaException(
400,
LambdaError.CreateRestrictionMissingBody,
)

payload = json.loads(body)

restricted_smartcard_id = payload.get("smartcardId")
nhs_number = payload.get("nhsNumber")
if not restricted_smartcard_id or not nhs_number:
logger.error("Missing required fields")
raise LambdaException(
400,
LambdaError.CreateRestrictionMissingFields,
)

return restricted_smartcard_id, nhs_number


@set_request_context_for_logging
@override_error_check
@ensure_environment_variables(
names=[
"RESTRICTIONS_TABLE_NAME",
"HEALTHCARE_WORKER_API_URL",
],
)
@handle_lambda_exceptions
@validate_patient_id
def lambda_handler(event, context):
request_context.app_interaction = LoggingAppInteraction.USER_RESTRICTION.value

feature_flag_service = FeatureFlagService()
feature_flag_service.validate_feature_flag(
FeatureFlags.USER_RESTRICTION_ENABLED,
)
logger.info("Starting create user restriction process")

restricted_smartcard_id, nhs_number = parse_body(event.get("body"))
request_context.patient_nhs_no = nhs_number

patient_id = extract_nhs_number_from_event(event)
if patient_id != nhs_number:
logger.error("patientId query param does not match nhs_number in request body")
raise LambdaException(
400,
LambdaError.PatientIdMismatch,
)

try:
creator, ods_code = extract_creator_and_ods_code_from_request_context()
except OdsErrorException:
logger.error("Missing user context")
raise LambdaException(
400,
LambdaError.CreateRestrictionMissingContext,
)

service = CreateUserRestrictionService()
try:
restriction_id = service.create_restriction(
restricted_smartcard_id=restricted_smartcard_id,
nhs_number=nhs_number,
custodian=ods_code,
creator=creator,
)
except UserRestrictionAlreadyExistsException as exc:
logger.error(exc)
raise LambdaException(
409,
LambdaError.CreateRestrictionAlreadyExists,
)
except HealthcareWorkerAPIException as exc:
logger.error(exc)
raise LambdaException(
400,
LambdaError.CreateRestrictionInvalidWorker,
)
except HealthcareWorkerPractitionerModelException as exc:
logger.error(exc)
raise LambdaException(
400,
LambdaError.CreateRestrictionPractitionerModelError,
)

return ApiGatewayResponse(
201,
json.dumps({"id": restriction_id}),
"POST",
).create_api_gateway_response()
Empty file.
6 changes: 6 additions & 0 deletions lambdas/services/authoriser_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ def deny_access_policy(self, path, http_verb, user_role, nhs_number: str = None)
case "/DocumentReview":
deny_resource = False

case "/UserRestriction":
if http_verb == HttpVerb.POST:
deny_resource = not patient_access_is_allowed
else:
deny_resource = False

case "/UploadState":
deny_resource = (
not patient_access_is_allowed or is_user_gp_clinical or is_user_pcse
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from enums.lambda_error import LambdaError
from models.user_restrictions.user_restrictions import UserRestriction
from services.user_restrictions.user_restriction_dynamo_service import (
UserRestrictionDynamoService,
)
from services.user_restrictions.utilities import get_healthcare_worker_api_service
from utils.audit_logging_setup import LoggingService
from utils.exceptions import (
UserRestrictionAlreadyExistsException,
)
from utils.lambda_exceptions import LambdaException
from utils.utilities import get_pds_service

logger = LoggingService(__name__)


class CreateUserRestrictionService:
def __init__(self):
self.dynamo_service = UserRestrictionDynamoService()
self.healthcare_service = get_healthcare_worker_api_service()
self.pds_service = get_pds_service()

def create_restriction(
self,
restricted_smartcard_id: str,
nhs_number: str,
custodian: str,
creator: str,
) -> str:
if restricted_smartcard_id == creator:
logger.error("You cannot create a restriction for yourself")
raise LambdaException(
400,
LambdaError.CreateRestrictionSelfRestriction,
)

patient = self.pds_service.fetch_patient_details(nhs_number)
if not patient:
logger.error("Patient not found in PDS")
raise LambdaException(
404,
LambdaError.SearchPatientNoPDS,
)
if patient.general_practice_ods != custodian:
logger.error(
"Patient's general practice ODS does not match request context ODS",
)
raise LambdaException(
403,
LambdaError.SearchPatientNoAuth,
)

existing = self.dynamo_service.get_active_restriction(
nhs_number=nhs_number,
restricted_user=restricted_smartcard_id,
)
if existing:
raise UserRestrictionAlreadyExistsException(
"A restriction already exists for this user and patient",
)

self.healthcare_service.get_practitioner(restricted_smartcard_id)

restriction = UserRestriction(
restricted_user=restricted_smartcard_id,
nhs_number=nhs_number,
custodian=custodian,
creator=creator,
)

self.dynamo_service.create_restriction_item(restriction)

logger.info("Created user restriction")
return restriction.id
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def process_request(
logger.info(f"Querying user restrictions for ODS code {ods_code}")
restrictions, next_token = self.dynamo_service.query_restrictions(
ods_code=ods_code,
smart_card_id=smartcard_id,
smartcard_id=smartcard_id,
nhs_number=nhs_number,
limit=limit,
start_key=next_page_token,
Expand Down
Loading
Loading