Skip to content
Draft
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
16 changes: 16 additions & 0 deletions .github/workflows/base-lambdas-reusable-deploy-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -880,3 +880,19 @@ jobs:
lambda_layer_names: "core_lambda_layer"
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

deploy_search_user_restriction_lambda:
name: Deploy Search 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_name: search_user_restriction_handler
lambda_handler_path: user_restrictions
lambda_aws_name: SearchUserRestriction
lambda_layer_names: "core_lambda_layer"
secrets:
AWS_ASSUME_ROLE: ${{ secrets.AWS_ASSUME_ROLE }}

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

class DocumentReviewQuerystringParameters(StrEnum):
LIMIT = "limit"
NHS_NUMBER = "nhsNumber"
NEXT_PAGE_TOKEN = "nextPageToken"
UPLOADER = "uploader"
NHS_NUMBER = "nhsNumber"
1 change: 1 addition & 0 deletions lambdas/enums/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ class FeatureFlags(StrEnum):
UPLOAD_DOCUMENT_ITERATION_2_ENABLED = "uploadDocumentIteration2Enabled"
UPLOAD_DOCUMENT_ITERATION_3_ENABLED = "uploadDocumentIteration3Enabled"
BULK_UPLOAD_SEND_TO_REVIEW_ENABLED = "bulkUploadSendToReviewEnabled"
USER_RESTRICTION_ENABLED = "userRestrictionEnabled"
20 changes: 20 additions & 0 deletions lambdas/enums/lambda_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -746,3 +746,23 @@ def create_error_body(
"err_code": "RD_IA",
"message": "Invalid action. Expected 'list' or 'process_one'.",
}

"""
Errors for User Restriction lambdas
"""
UserRestrictionMissingODS = {
"err_code": "SUR_4001",
"message": "No ODS code provided in request context",
}
UserRestrictionInvalidQueryString = {
"err_code": "SUR_4002",
"message": "Invalid query string parameter",
}
UserRestrictionDB = {
"err_code": "SUR_5001",
"message": "Failed to query user restrictions from DynamoDB",
}
UserRestrictionInvalidQueryParameter = {
"err_code": "SUR_4003",
"message": "Invalid query parameter value: %(details)s",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from enum import StrEnum


class UserRestrictionQuerystringParameters(StrEnum):
LIMIT = "limit"
NHS_NUMBER = "nhsNumber"
SMARTCARD_ID = "smartcardId"
NEXT_PAGE_TOKEN = "nextPageToken"
8 changes: 5 additions & 3 deletions lambdas/handlers/mns_notification_handler.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import json

from pydantic import ValidationError

from enums.mns_notification_types import MNSNotificationTypes
from models.sqs.mns_sqs_message import MNSSQSMessage
from pydantic import ValidationError
from services.process_mns_message_service import MNSNotificationService
from utils.audit_logging_setup import LoggingService
from utils.decorators.ensure_env_var import ensure_environment_variables
Expand All @@ -21,7 +22,8 @@
"LLOYD_GEORGE_DYNAMODB_NAME",
"DOCUMENT_REVIEW_DYNAMODB_NAME",
"MNS_NOTIFICATION_QUEUE_URL",
]
"RESTRICTIONS_TABLE_NAME",
],
)
@override_error_check
def lambda_handler(event, context):
Expand All @@ -38,7 +40,7 @@ def lambda_handler(event, context):

request_context.patient_nhs_no = mns_message.subject.nhs_number
logger.info(
f"Processing SQS message for nhs number: {mns_message.subject.nhs_number}"
f"Processing SQS message for nhs number: {mns_message.subject.nhs_number}",
)

if mns_message.type in MNSNotificationTypes.__members__.values():
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import json

from enums.feature_flags import FeatureFlags
from enums.lambda_error import LambdaError
from enums.user_restriction_accepted_querystring_parameters import (
UserRestrictionQuerystringParameters,
)
from services.feature_flags_service import FeatureFlagService
from services.user_restrictions.search_user_restriction_service import (
DEFAULT_LIMIT,
SearchUserRestrictionService,
)
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.exceptions import (
InvalidParamException,
OdsErrorException,
UserRestrictionException,
UserRestrictionValidationException,
)
from utils.lambda_response import ApiGatewayResponse
from utils.ods_utils import extract_ods_code_from_request_context

logger = LoggingService(__name__)


@set_request_context_for_logging
@ensure_environment_variables(
names=[
"RESTRICTIONS_TABLE_NAME",
"APPCONFIG_APPLICATION",
"APPCONFIG_CONFIGURATION",
"APPCONFIG_ENVIRONMENT",
"HEALTHCARE_WORKER_API_URL",
],
)
@override_error_check
@handle_lambda_exceptions
def lambda_handler(event, _context):
try:
feature_flag_service = FeatureFlagService()
feature_flag_service.validate_feature_flag(
FeatureFlags.USER_RESTRICTION_ENABLED.value,
)
ods_code = extract_ods_code_from_request_context()

params = parse_querystring_parameters(event)

service = SearchUserRestrictionService()

restrictions, next_token = service.process_request(
ods_code=ods_code,
smartcard_id=params.get(UserRestrictionQuerystringParameters.SMARTCARD_ID),
nhs_number=params.get(UserRestrictionQuerystringParameters.NHS_NUMBER),
next_page_token=params.get(
UserRestrictionQuerystringParameters.NEXT_PAGE_TOKEN,
),
limit=params.get(UserRestrictionQuerystringParameters.LIMIT, DEFAULT_LIMIT),
)

response_body: dict = {
"restrictions": restrictions,
"count": len(restrictions),
}

if next_token:
response_body["nextPageToken"] = next_token

return ApiGatewayResponse(
status_code=200,
body=json.dumps(response_body),
methods="GET",
).create_api_gateway_response()

except OdsErrorException as e:
logger.error(e)
return ApiGatewayResponse(
status_code=401,
body=LambdaError.UserRestrictionMissingODS.create_error_body(),
methods="GET",
).create_api_gateway_response()

except (UserRestrictionException, InvalidParamException) as e:
logger.error(f"Invalid query parameter: {e}")
return ApiGatewayResponse(
status_code=400,
body=LambdaError.UserRestrictionInvalidQueryParameter.create_error_body(),
methods="GET",
).create_api_gateway_response()

except UserRestrictionValidationException as e:
logger.error(f"Restriction model validation error: {e}")
return ApiGatewayResponse(
status_code=500,
body=LambdaError.UserRestrictionDB.create_error_body(),
methods="GET",
).create_api_gateway_response()


def parse_querystring_parameters(event: dict) -> dict:
logger.info("Parsing query string parameters.")
params = event.get("queryStringParameters") or {}

return {
param.value: params[param.value]
for param in UserRestrictionQuerystringParameters
if param.value in params
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from models.user_restrictions.user_restrictions import UserRestriction


class UserRestrictionResponse(UserRestriction):
patient_given_name: list[str] | None = None
patient_family_name: str | None = None
restricted_user_first_name: str | None = None
restricted_user_last_name: str | None = None
12 changes: 11 additions & 1 deletion lambdas/models/user_restrictions/user_restrictions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,17 @@ class UserRestrictionsFields(StrEnum):
ID = "ID"
CREATOR = "CreatorSmartcard"
RESTRICTED_USER = "RestrictedSmartcard"
REMOVED_BY = "RemoverSmartCard"
REMOVED_BY = "RemoverSmartcard"
NHS_NUMBER = "NhsNumber"
CUSTODIAN = "Custodian"
IS_ACTIVE = "IsActive"
LAST_UPDATED = "LastUpdated"
CREATED = "Created"


class UserRestrictionIndexes(StrEnum):
CUSTODIAN_INDEX = "CustodianIndex"
NHS_NUMBER_INDEX = "NhsNumberIndex"


class UserRestriction(BaseModel):
Expand Down
7 changes: 7 additions & 0 deletions lambdas/services/authoriser_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
except (KeyError, IndexError) as e:
raise AuthorisationException(e)

def deny_access_policy(self, path, http_verb, user_role, nhs_number: str = None):

Check failure on line 81 in lambdas/services/authoriser_service.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=NHSDigital_national-document-repository&issues=AZzYBH1C-jsa3wBjvhut&open=AZzYBH1C-jsa3wBjvhut&pullRequest=1164
logger.info(f"Path: {path}")

patient_access_is_allowed = (
Expand Down Expand Up @@ -149,6 +149,13 @@
deny_resource = (
not patient_access_is_allowed or is_user_gp_clinical or is_user_pcse
)
case "/UserRestriction":
if http_verb == HttpVerb.POST:
deny_resource = not patient_access_is_allowed
elif http_verb == HttpVerb.GET:
deny_resource = False
else:
deny_resource = True

case "/VirusScan":
deny_resource = (
Expand Down
7 changes: 4 additions & 3 deletions lambdas/services/base/dynamo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import boto3
from boto3.dynamodb.conditions import Attr, ConditionBase, Key
from botocore.exceptions import ClientError

from utils.audit_logging_setup import LoggingService
from utils.dynamo_utils import (
create_expression_attribute_values,
Expand Down Expand Up @@ -184,7 +185,7 @@ def create_item(self, table_name, item, key_name: str | None = None):
Args:
table_name: Name of the DynamoDB table
item: The item to be inserted (as a dictionary)
key_name: The name of the key field to check existance for conditional put
key_name: The name of the key field to check existence for conditional put
Returns:
Response from the DynamoDB put_item operation
Raises:
Expand Down Expand Up @@ -370,7 +371,7 @@ def transact_write_items(self, transact_items: Sequence[dict]):
transact_items: List of transaction items (Put, Update, Delete, ConditionCheck)

Raises:
ClientError: If the transaction fails (e.g., TransactionCanceledException)
ClientError: If the transaction fails (e.g. TransactionCanceledException)
"""
try:
logger.info(f"Executing transaction with {len(transact_items)} items")
Expand Down Expand Up @@ -497,7 +498,7 @@ def query_table_with_paginator(
key: str,
condition: str,
filter_expression: str | None = None,
expression_attribute_names: str | None = None,
expression_attribute_names: dict | None = None,
expression_attribute_values: dict | None = None,
limit: int = 20,
page_size: int = 1,
Expand Down
Loading
Loading