diff --git a/infrastructure/README.md b/infrastructure/README.md index 615f880c..75609bdc 100644 --- a/infrastructure/README.md +++ b/infrastructure/README.md @@ -87,6 +87,10 @@ | [manage-nrl-pointer-alarm](#module\_manage-nrl-pointer-alarm) | ./modules/lambda_alarms | n/a | | [manage-nrl-pointer-alarm-topic](#module\_manage-nrl-pointer-alarm-topic) | ./modules/sns | n/a | | [manage-nrl-pointer-lambda](#module\_manage-nrl-pointer-lambda) | ./modules/lambda | n/a | +| [mns-notification-alarm](#module\_mns-notification-alarm) | ./modules/lambda_alarms | n/a | +| [mns-notification-alarm-topic](#module\_mns-notification-alarm-topic) | ./modules/sns | n/a | +| [mns-notification-lambda](#module\_mns-notification-lambda) | ./modules/lambda | n/a | +| [mns\_encryption\_key](#module\_mns\_encryption\_key) | ./modules/kms | n/a | | [ndr-app-config](#module\_ndr-app-config) | ./modules/app_config | n/a | | [ndr-bulk-staging-store](#module\_ndr-bulk-staging-store) | ./modules/s3/ | n/a | | [ndr-docker-ecr-ui](#module\_ndr-docker-ecr-ui) | ./modules/ecr/ | n/a | @@ -119,6 +123,7 @@ | [sns\_encryption\_key](#module\_sns\_encryption\_key) | ./modules/kms | n/a | | [sqs-lg-bulk-upload-invalid-queue](#module\_sqs-lg-bulk-upload-invalid-queue) | ./modules/sqs | n/a | | [sqs-lg-bulk-upload-metadata-queue](#module\_sqs-lg-bulk-upload-metadata-queue) | ./modules/sqs | n/a | +| [sqs-mns-notification-queue](#module\_sqs-mns-notification-queue) | ./modules/sqs | n/a | | [sqs-nems-queue](#module\_sqs-nems-queue) | ./modules/sqs | n/a | | [sqs-nrl-queue](#module\_sqs-nrl-queue) | ./modules/sqs | n/a | | [sqs-splunk-queue](#module\_sqs-splunk-queue) | ./modules/sqs | n/a | @@ -181,6 +186,7 @@ | [aws_iam_policy.dynamodb_policy_scan_bulk_report](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.dynamodb_stream_manifest](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.dynamodb_stream_stitch_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.kms_lambda_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.lambda_audit_splunk_sqs_queue_send_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.s3_document_data_policy_for_manifest_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_policy.s3_document_data_policy_for_stitch_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | @@ -219,6 +225,7 @@ | [aws_lambda_event_source_mapping.bulk_upload_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | | [aws_lambda_event_source_mapping.dynamodb_stream_manifest](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | | [aws_lambda_event_source_mapping.dynamodb_stream_stitch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | +| [aws_lambda_event_source_mapping.mns_notification_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | | [aws_lambda_event_source_mapping.nems_message_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | | [aws_lambda_event_source_mapping.nrl_pointer_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_event_source_mapping) | resource | | [aws_lambda_permission.bulk_upload_metadata_schedule_permission](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | @@ -232,6 +239,7 @@ | [aws_security_group.ndr_mesh_sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_sns_topic.alarm_notifications_topic](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | | [aws_sns_topic_subscription.alarm_notifications_sns_topic_subscription](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource | +| [aws_sqs_queue_policy.mns_sqs_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | | [aws_sqs_queue_policy.nems_events_subscription](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sqs_queue_policy) | resource | | [aws_ssm_parameter.nems_events_observability](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | | [aws_ssm_parameter.nems_events_topic_arn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | resource | @@ -263,6 +271,7 @@ | [aws_ssm_parameter.backup_target_account](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.cloud_security_notification_email_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.end_user_ods_code](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.mns_lambda_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.splunk_trusted_principal](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.target_backup_vault_arn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | @@ -272,7 +281,7 @@ |------|-------------|------|---------|:--------:| | [auth\_session\_dynamodb\_table\_name](#input\_auth\_session\_dynamodb\_table\_name) | The name of dynamodb table to store user login sessions | `string` | `"AuthSessionReferenceMetadata"` | no | | [auth\_state\_dynamodb\_table\_name](#input\_auth\_state\_dynamodb\_table\_name) | The name of dynamodb table to store the state values (for CIS2 authorisation) | `string` | `"AuthStateReferenceMetadata"` | no | -| [availability\_zones](#input\_availability\_zones) | This is a list that specifies all the Availability Zones that will have a pair of public and private subnets | `list(string)` |
[
"eu-west-2a",
"eu-west-2b",
"eu-west-2c"
]
| no | +| [availability\_zones](#input\_availability\_zones) | This is a list that specifies all the Availability Zones that will have a pair of public and private subnets | `list(string)` |
[
"eu-west-2a",
"eu-west-2b",
"eu-west-2c"
]
| no | | [bulk\_upload\_report\_dynamodb\_table\_name](#input\_bulk\_upload\_report\_dynamodb\_table\_name) | The name of dynamodb table to store bulk upload status | `string` | `"BulkUploadReport"` | no | | [certificate\_domain](#input\_certificate\_domain) | n/a | `string` | n/a | yes | | [certificate\_subdomain\_name\_prefix](#input\_certificate\_subdomain\_name\_prefix) | Prefix to add to subdomains on certification configurations, dev envs use api-{env}, prod envs use api.{env} | `string` | `"api-"` | no | diff --git a/infrastructure/kms_sns.tf b/infrastructure/kms_sns.tf index 1c60a880..8c9fe1ea 100644 --- a/infrastructure/kms_sns.tf +++ b/infrastructure/kms_sns.tf @@ -5,5 +5,5 @@ module "sns_encryption_key" { current_account_id = data.aws_caller_identity.current.account_id environment = var.environment owner = var.owner - identifiers = ["sns.amazonaws.com", "cloudwatch.amazonaws.com"] + service_identifiers = ["sns.amazonaws.com", "cloudwatch.amazonaws.com"] } \ No newline at end of file diff --git a/infrastructure/lambda-mns-notification.tf b/infrastructure/lambda-mns-notification.tf new file mode 100644 index 00000000..39006e84 --- /dev/null +++ b/infrastructure/lambda-mns-notification.tf @@ -0,0 +1,97 @@ +module "mns-notification-lambda" { + source = "./modules/lambda" + name = "MNSNotificationLambda" + handler = "handlers.mns_notification_handler.lambda_handler" + iam_role_policy_documents = [ + module.sqs-mns-notification-queue.sqs_read_policy_document, + module.sqs-mns-notification-queue.sqs_write_policy_document, + module.lloyd_george_reference_dynamodb_table.dynamodb_write_policy_document, + module.lloyd_george_reference_dynamodb_table.dynamodb_read_policy_document, + aws_iam_policy.ssm_access_policy.policy, + module.ndr-app-config.app_config_policy, + aws_iam_policy.kms_lambda_access.policy, + ] + rest_api_id = null + api_execution_arn = null + + lambda_environment_variables = { + APPCONFIG_APPLICATION = module.ndr-app-config.app_config_application_id + APPCONFIG_ENVIRONMENT = module.ndr-app-config.app_config_environment_id + APPCONFIG_CONFIGURATION = module.ndr-app-config.app_config_configuration_profile_id + WORKSPACE = terraform.workspace + LLOYD_GEORGE_DYNAMODB_NAME = "${terraform.workspace}_${var.lloyd_george_dynamodb_table_name}" + MNS_NOTIFICATION_QUEUE_URL = module.sqs-mns-notification-queue.sqs_url + PDS_FHIR_IS_STUBBED = local.is_sandbox + } + + is_gateway_integration_needed = false + is_invoked_from_gateway = false + lambda_timeout = 900 + reserved_concurrent_executions = local.mns_notification_lambda_concurrent_limit +} + +resource "aws_lambda_event_source_mapping" "mns_notification_lambda" { + event_source_arn = module.sqs-mns-notification-queue.endpoint + function_name = module.mns-notification-lambda.lambda_arn + + scaling_config { + maximum_concurrency = local.mns_notification_lambda_concurrent_limit + } +} + +module "mns-notification-alarm" { + source = "./modules/lambda_alarms" + lambda_function_name = module.mns-notification-lambda.function_name + lambda_timeout = module.mns-notification-lambda.timeout + lambda_name = "mns_notification_handler" + namespace = "AWS/Lambda" + alarm_actions = [module.mns-notification-alarm-topic.arn] + ok_actions = [module.mns-notification-alarm-topic.arn] +} + +module "mns-notification-alarm-topic" { + source = "./modules/sns" + sns_encryption_key_id = module.sns_encryption_key.id + current_account_id = data.aws_caller_identity.current.account_id + topic_name = "mns-notification-topic" + topic_protocol = "lambda" + topic_endpoint = module.mns-notification-lambda.lambda_arn + delivery_policy = jsonencode({ + "Version" : "2012-10-17", + "Statement" : [ + { + "Effect" : "Allow", + "Principal" : { + "Service" : "cloudwatch.amazonaws.com" + }, + "Action" : [ + "SNS:Publish", + ], + "Condition" : { + "ArnLike" : { + "aws:SourceArn" : "arn:aws:cloudwatch:eu-west-2:${data.aws_caller_identity.current.account_id}:alarm:*" + } + } + "Resource" : "*" + } + ] + }) +} + +resource "aws_iam_policy" "kms_lambda_access" { + name = "mns_notification_lambda_access_policy" + description = "KMS policy to allow lambda to read MNS SQS messages" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "kms:Decrypt", + ] + Effect = "Allow" + Resource = module.mns_encryption_key.kms_arn + }, + ] + }) +} \ No newline at end of file diff --git a/infrastructure/mns.tf b/infrastructure/mns.tf new file mode 100644 index 00000000..1e21592e --- /dev/null +++ b/infrastructure/mns.tf @@ -0,0 +1,47 @@ +data "aws_ssm_parameter" "mns_lambda_role" { + name = "/ndr/${var.environment}/mns/lambda_role" +} + + +module "mns_encryption_key" { + source = "./modules/kms" + kms_key_name = "alias/mns-notification-encryption-key-kms-${terraform.workspace}" + kms_key_description = "Custom KMS Key to enable server side encryption for mns subscriptions" + current_account_id = data.aws_caller_identity.current.account_id + environment = var.environment + owner = var.owner + service_identifiers = ["sns.amazonaws.com"] + aws_identifiers = [data.aws_ssm_parameter.mns_lambda_role.value] + allow_decrypt_for_arn = true +} + +module "sqs-mns-notification-queue" { + source = "./modules/sqs" + name = "mns-notification-queue" + max_size_message = 256 * 1024 # allow message size up to 256 KB + message_retention = 60 * 60 * 24 * 14 # 14 days + environment = var.environment + owner = var.owner + max_visibility = 1020 + delay = 60 + enable_sse = null + kms_master_key_id = module.mns_encryption_key.id +} + +resource "aws_sqs_queue_policy" "mns_sqs_access" { + queue_url = module.sqs-mns-notification-queue.sqs_url + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow", + Principal = { + AWS = data.aws_ssm_parameter.mns_lambda_role.value + }, + Action = "SQS:SendMessage", + Resource = module.sqs-mns-notification-queue.sqs_arn + } + ] + }) +} diff --git a/infrastructure/modules/dynamo_db/main.tf b/infrastructure/modules/dynamo_db/main.tf index bcbbeb28..74d6ac2b 100644 --- a/infrastructure/modules/dynamo_db/main.tf +++ b/infrastructure/modules/dynamo_db/main.tf @@ -53,8 +53,8 @@ resource "aws_iam_policy" "dynamodb_policy" { Statement = concat( [ { - "Effect" : "Allow", - "Action" : [ + Effect : "Allow", + Action : [ "dynamodb:Query", "dynamodb:Scan", "dynamodb:GetItem", @@ -63,18 +63,18 @@ resource "aws_iam_policy" "dynamodb_policy" { "dynamodb:DeleteItem", "dynamodb:BatchWriteItem", ], - "Resource" : [ + Resource : [ aws_dynamodb_table.ndr_dynamodb_table.arn, ] } ], length(coalesce(var.global_secondary_indexes, [])) > 0 ? [ { - "Effect" : "Allow", - "Action" : [ + Effect : "Allow", + Action : [ "dynamodb:Query", ], - "Resource" : [ + Resource : [ for index in var.global_secondary_indexes : "${aws_dynamodb_table.ndr_dynamodb_table.arn}/index/${index.name}" ] diff --git a/infrastructure/modules/ecs/README.md b/infrastructure/modules/ecs/README.md index 990ee670..d0ecfecf 100644 --- a/infrastructure/modules/ecs/README.md +++ b/infrastructure/modules/ecs/README.md @@ -63,7 +63,7 @@ No modules. | [ecs\_task\_definition\_cpu](#input\_ecs\_task\_definition\_cpu) | n/a | `number` | `1024` | no | | [ecs\_task\_definition\_memory](#input\_ecs\_task\_definition\_memory) | n/a | `number` | `2048` | no | | [environment](#input\_environment) | n/a | `string` | n/a | yes | -| [environment\_vars](#input\_environment\_vars) | n/a | `list` |
[
null
]
| no | +| [environment\_vars](#input\_environment\_vars) | n/a | `list` |
[
null
]
| no | | [is\_autoscaling\_needed](#input\_is\_autoscaling\_needed) | n/a | `bool` | `true` | no | | [is\_lb\_needed](#input\_is\_lb\_needed) | n/a | `bool` | `false` | no | | [is\_service\_needed](#input\_is\_service\_needed) | n/a | `bool` | `true` | no | diff --git a/infrastructure/modules/kms/README.md b/infrastructure/modules/kms/README.md index 0e314990..77172e9a 100644 --- a/infrastructure/modules/kms/README.md +++ b/infrastructure/modules/kms/README.md @@ -18,19 +18,24 @@ No modules. |------|------| | [aws_kms_alias.encryption_key_alias](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | | [aws_kms_key.encryption_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | -| [aws_iam_policy_document.kms_key_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.combined_policy_documents](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.kms_key_base](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.kms_key_generate](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [allow\_decrypt\_for\_arn](#input\_allow\_decrypt\_for\_arn) | n/a | `bool` | `false` | no | +| [allowed\_arn](#input\_allowed\_arn) | n/a | `list(string)` | `[]` | no | +| [aws\_identifiers](#input\_aws\_identifiers) | n/a | `list(string)` | `[]` | no | | [current\_account\_id](#input\_current\_account\_id) | n/a | `string` | n/a | yes | | [environment](#input\_environment) | n/a | `string` | n/a | yes | -| [identifiers](#input\_identifiers) | n/a | `list(string)` | n/a | yes | | [kms\_key\_description](#input\_kms\_key\_description) | n/a | `string` | n/a | yes | | [kms\_key\_name](#input\_kms\_key\_name) | n/a | `string` | n/a | yes | | [kms\_key\_rotation\_enabled](#input\_kms\_key\_rotation\_enabled) | n/a | `bool` | `true` | no | | [owner](#input\_owner) | n/a | `string` | n/a | yes | +| [service\_identifiers](#input\_service\_identifiers) | n/a | `list(string)` | n/a | yes | ## Outputs diff --git a/infrastructure/modules/kms/main.tf b/infrastructure/modules/kms/main.tf index 17823337..4bd4ff85 100644 --- a/infrastructure/modules/kms/main.tf +++ b/infrastructure/modules/kms/main.tf @@ -1,6 +1,6 @@ resource "aws_kms_key" "encryption_key" { description = var.kms_key_description - policy = data.aws_iam_policy_document.kms_key_policy_doc.json + policy = data.aws_iam_policy_document.combined_policy_documents.json enable_key_rotation = var.kms_key_rotation_enabled tags = { @@ -17,7 +17,7 @@ resource "aws_kms_alias" "encryption_key_alias" { } -data "aws_iam_policy_document" "kms_key_policy_doc" { +data "aws_iam_policy_document" "kms_key_base" { statement { effect = "Allow" principals { @@ -30,7 +30,7 @@ data "aws_iam_policy_document" "kms_key_policy_doc" { statement { effect = "Allow" principals { - identifiers = var.identifiers + identifiers = var.service_identifiers type = "Service" } actions = [ @@ -38,5 +38,34 @@ data "aws_iam_policy_document" "kms_key_policy_doc" { "kms:GenerateDataKey*" ] resources = ["*"] + dynamic "condition" { + for_each = var.allow_decrypt_for_arn ? [1] : [] + content { + test = "ArnEquals" + values = var.allowed_arn + variable = "aws:SourceArn" + } + } + } +} + +data "aws_iam_policy_document" "kms_key_generate" { + count = length(var.aws_identifiers) > 0 ? 1 : 0 + statement { + effect = "Allow" + principals { + identifiers = var.aws_identifiers + type = "AWS" + } + actions = ["kms:GenerateDataKey"] + resources = ["*"] } -} \ No newline at end of file +} + +data "aws_iam_policy_document" "combined_policy_documents" { + source_policy_documents = flatten([ + data.aws_iam_policy_document.kms_key_base.json, + length(var.aws_identifiers) > 0 ? [data.aws_iam_policy_document.kms_key_generate[0].json] : [] + ]) +} + diff --git a/infrastructure/modules/kms/variable.tf b/infrastructure/modules/kms/variable.tf index e3d632f7..2b9111aa 100644 --- a/infrastructure/modules/kms/variable.tf +++ b/infrastructure/modules/kms/variable.tf @@ -23,14 +23,29 @@ variable "owner" { type = string } -variable "identifiers" { +variable "service_identifiers" { type = list(string) } +variable "aws_identifiers" { + type = list(string) + default = [] +} + +variable "allow_decrypt_for_arn" { + type = bool + default = false +} + +variable "allowed_arn" { + type = list(string) + default = [] +} + output "kms_arn" { value = aws_kms_key.encryption_key.arn } output "id" { value = aws_kms_key.encryption_key.id -} \ No newline at end of file +} diff --git a/infrastructure/modules/lambda/README.md b/infrastructure/modules/lambda/README.md index 16db8fc5..f264df7c 100644 --- a/infrastructure/modules/lambda/README.md +++ b/infrastructure/modules/lambda/README.md @@ -33,7 +33,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| | [api\_execution\_arn](#input\_api\_execution\_arn) | n/a | `string` | n/a | yes | -| [default\_policies](#input\_default\_policies) | n/a | `list` |
[
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
"arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy"
]
| no | +| [default\_policies](#input\_default\_policies) | n/a | `list` |
[
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
"arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy"
]
| no | | [handler](#input\_handler) | n/a | `string` | n/a | yes | | [http\_methods](#input\_http\_methods) | n/a | `list(string)` | `[]` | no | | [iam\_role\_policy\_documents](#input\_iam\_role\_policy\_documents) | n/a | `list(string)` | `[]` | no | diff --git a/infrastructure/queues.tf b/infrastructure/queues.tf index 34c13a4e..7c1d9b11 100644 --- a/infrastructure/queues.tf +++ b/infrastructure/queues.tf @@ -29,4 +29,3 @@ module "sqs-lg-bulk-upload-invalid-queue" { owner = var.owner max_visibility = 1020 } - diff --git a/infrastructure/variable.tf b/infrastructure/variable.tf index e6245088..4d81d337 100644 --- a/infrastructure/variable.tf +++ b/infrastructure/variable.tf @@ -226,7 +226,8 @@ locals { is_force_destroy = contains(["ndr-dev", "ndra", "ndrb", "ndrc", "ndrd", "ndr-test"], terraform.workspace) is_sandbox_or_test = contains(["ndra", "ndrb", "ndrc", "ndrd", "ndr-test"], terraform.workspace) - bulk_upload_lambda_concurrent_limit = 5 + bulk_upload_lambda_concurrent_limit = 5 + mns_notification_lambda_concurrent_limit = 3 api_gateway_subdomain_name = contains(["prod"], terraform.workspace) ? "${var.certificate_subdomain_name_prefix}" : "${var.certificate_subdomain_name_prefix}${terraform.workspace}" api_gateway_full_domain_name = contains(["prod"], terraform.workspace) ? "${var.certificate_subdomain_name_prefix}${var.domain}" : "${var.certificate_subdomain_name_prefix}${terraform.workspace}.${var.domain}"