diff --git a/CHANGELOG.md b/CHANGELOG.md index a43a1c66c..a2e67273b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2025-03-07 +- [PI-383] External ID +- [PI-838] product_team includes product_team_id + ## 2025-03-05 - [PI-833] Read product by productID - [PI-832] product_team_id as a key in product_team diff --git a/VERSION b/VERSION index 0ad773553..82166197a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2025.03.05 +2025.03.07 diff --git a/changelog/2025-03-07.md b/changelog/2025-03-07.md new file mode 100644 index 000000000..b08ee1f51 --- /dev/null +++ b/changelog/2025-03-07.md @@ -0,0 +1,2 @@ +- [PI-383] External ID +- [PI-838] product_team includes product_team_id diff --git a/infrastructure/swagger/07_components--schemas--domain.yaml b/infrastructure/swagger/07_components--schemas--domain.yaml index d1e2b1801..4de100633 100644 --- a/infrastructure/swagger/07_components--schemas--domain.yaml +++ b/infrastructure/swagger/07_components--schemas--domain.yaml @@ -83,6 +83,8 @@ components: type: string cpm_product_team_id: type: string + product_team_id: + type: string ods_code: type: string status: @@ -107,7 +109,8 @@ components: example: id: "P.1X3-XXX" name: "Sample Product" - cpm_product_team_id: "55e86121-3826-468c-a6f0-dd0f1fbc0259" + product_team_id: "55e86121-3826-468c-a6f0-dd0f1fbc0259" + cpm_product_team_id: "a9a9694d-001b-45ce-9f2a-6c9bf80ae0d0" ods_code: "F5H1R" keys: [] status: "active" diff --git a/infrastructure/terraform/per_account/dev/parameters/main.tf b/infrastructure/terraform/per_account/dev/parameters/main.tf index 31e2409b2..fd825708b 100644 --- a/infrastructure/terraform/per_account/dev/parameters/main.tf +++ b/infrastructure/terraform/per_account/dev/parameters/main.tf @@ -66,3 +66,7 @@ resource "aws_secretsmanager_secret" "apigee-app-client-info" { resource "aws_secretsmanager_secret" "apigee-sds-app-key" { name = "${terraform.workspace}-apigee-sds-app-key" } + +resource "aws_secretsmanager_secret" "external-id" { + name = "${terraform.workspace}-external-id" +} diff --git a/infrastructure/terraform/per_account/dev/parameters/provider.tf b/infrastructure/terraform/per_account/dev/parameters/provider.tf index 7d8a50a58..2f6b53001 100644 --- a/infrastructure/terraform/per_account/dev/parameters/provider.tf +++ b/infrastructure/terraform/per_account/dev/parameters/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/dev/parameters/vars.tf b/infrastructure/terraform/per_account/dev/parameters/vars.tf index 8b585d3c0..8d34250b8 100644 --- a/infrastructure/terraform/per_account/dev/parameters/vars.tf +++ b/infrastructure/terraform/per_account/dev/parameters/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "deletion_protection_enabled" { diff --git a/infrastructure/terraform/per_account/dev/provider.tf b/infrastructure/terraform/per_account/dev/provider.tf index aa11ecde0..e8b47641a 100644 --- a/infrastructure/terraform/per_account/dev/provider.tf +++ b/infrastructure/terraform/per_account/dev/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/dev/vars.tf b/infrastructure/terraform/per_account/dev/vars.tf index 32d0505a7..fb125e7ca 100644 --- a/infrastructure/terraform/per_account/dev/vars.tf +++ b/infrastructure/terraform/per_account/dev/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "expiration_date" { diff --git a/infrastructure/terraform/per_account/int/parameters/main.tf b/infrastructure/terraform/per_account/int/parameters/main.tf index 9604c7bb6..e1976110a 100644 --- a/infrastructure/terraform/per_account/int/parameters/main.tf +++ b/infrastructure/terraform/per_account/int/parameters/main.tf @@ -61,3 +61,7 @@ resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { resource "aws_secretsmanager_secret" "apigee-sds-app-key" { name = "${terraform.workspace}-apigee-sds-app-key" } + +resource "aws_secretsmanager_secret" "external-id" { + name = "${terraform.workspace}-external-id" +} diff --git a/infrastructure/terraform/per_account/int/parameters/provider.tf b/infrastructure/terraform/per_account/int/parameters/provider.tf index 7d8a50a58..2f6b53001 100644 --- a/infrastructure/terraform/per_account/int/parameters/provider.tf +++ b/infrastructure/terraform/per_account/int/parameters/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/int/parameters/vars.tf b/infrastructure/terraform/per_account/int/parameters/vars.tf index 8b585d3c0..8d34250b8 100644 --- a/infrastructure/terraform/per_account/int/parameters/vars.tf +++ b/infrastructure/terraform/per_account/int/parameters/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "deletion_protection_enabled" { diff --git a/infrastructure/terraform/per_account/int/provider.tf b/infrastructure/terraform/per_account/int/provider.tf index aa11ecde0..e8b47641a 100644 --- a/infrastructure/terraform/per_account/int/provider.tf +++ b/infrastructure/terraform/per_account/int/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/int/vars.tf b/infrastructure/terraform/per_account/int/vars.tf index 0f8001bd6..efa9b57e6 100644 --- a/infrastructure/terraform/per_account/int/vars.tf +++ b/infrastructure/terraform/per_account/int/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "expiration_date" { diff --git a/infrastructure/terraform/per_account/prod/parameters/main.tf b/infrastructure/terraform/per_account/prod/parameters/main.tf index a2fe11bc2..fc8dd64e5 100644 --- a/infrastructure/terraform/per_account/prod/parameters/main.tf +++ b/infrastructure/terraform/per_account/prod/parameters/main.tf @@ -62,3 +62,7 @@ resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { resource "aws_secretsmanager_secret" "apigee-sds-app-key" { name = "${terraform.workspace}-apigee-sds-app-key" } + +resource "aws_secretsmanager_secret" "external-id" { + name = "${terraform.workspace}-external-id" +} diff --git a/infrastructure/terraform/per_account/prod/parameters/provider.tf b/infrastructure/terraform/per_account/prod/parameters/provider.tf index 7d8a50a58..2f6b53001 100644 --- a/infrastructure/terraform/per_account/prod/parameters/provider.tf +++ b/infrastructure/terraform/per_account/prod/parameters/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/prod/parameters/vars.tf b/infrastructure/terraform/per_account/prod/parameters/vars.tf index 8b585d3c0..8d34250b8 100644 --- a/infrastructure/terraform/per_account/prod/parameters/vars.tf +++ b/infrastructure/terraform/per_account/prod/parameters/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "deletion_protection_enabled" { diff --git a/infrastructure/terraform/per_account/prod/provider.tf b/infrastructure/terraform/per_account/prod/provider.tf index aa11ecde0..e8b47641a 100644 --- a/infrastructure/terraform/per_account/prod/provider.tf +++ b/infrastructure/terraform/per_account/prod/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/prod/vars.tf b/infrastructure/terraform/per_account/prod/vars.tf index b6a6584c7..3c3029cd3 100644 --- a/infrastructure/terraform/per_account/prod/vars.tf +++ b/infrastructure/terraform/per_account/prod/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "expiration_date" { diff --git a/infrastructure/terraform/per_account/qa/parameters/main.tf b/infrastructure/terraform/per_account/qa/parameters/main.tf index a2fe11bc2..fc8dd64e5 100644 --- a/infrastructure/terraform/per_account/qa/parameters/main.tf +++ b/infrastructure/terraform/per_account/qa/parameters/main.tf @@ -62,3 +62,7 @@ resource "aws_secretsmanager_secret" "etl_notify_slack_webhook_url" { resource "aws_secretsmanager_secret" "apigee-sds-app-key" { name = "${terraform.workspace}-apigee-sds-app-key" } + +resource "aws_secretsmanager_secret" "external-id" { + name = "${terraform.workspace}-external-id" +} diff --git a/infrastructure/terraform/per_account/qa/parameters/provider.tf b/infrastructure/terraform/per_account/qa/parameters/provider.tf index 7d8a50a58..2f6b53001 100644 --- a/infrastructure/terraform/per_account/qa/parameters/provider.tf +++ b/infrastructure/terraform/per_account/qa/parameters/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/qa/parameters/vars.tf b/infrastructure/terraform/per_account/qa/parameters/vars.tf index 8b585d3c0..8d34250b8 100644 --- a/infrastructure/terraform/per_account/qa/parameters/vars.tf +++ b/infrastructure/terraform/per_account/qa/parameters/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "deletion_protection_enabled" { diff --git a/infrastructure/terraform/per_account/qa/provider.tf b/infrastructure/terraform/per_account/qa/provider.tf index aa11ecde0..e8b47641a 100644 --- a/infrastructure/terraform/per_account/qa/provider.tf +++ b/infrastructure/terraform/per_account/qa/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/qa/vars.tf b/infrastructure/terraform/per_account/qa/vars.tf index d491db449..6925a3a26 100644 --- a/infrastructure/terraform/per_account/qa/vars.tf +++ b/infrastructure/terraform/per_account/qa/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "expiration_date" { diff --git a/infrastructure/terraform/per_account/ref/parameters/main.tf b/infrastructure/terraform/per_account/ref/parameters/main.tf index 31e2409b2..fd825708b 100644 --- a/infrastructure/terraform/per_account/ref/parameters/main.tf +++ b/infrastructure/terraform/per_account/ref/parameters/main.tf @@ -66,3 +66,7 @@ resource "aws_secretsmanager_secret" "apigee-app-client-info" { resource "aws_secretsmanager_secret" "apigee-sds-app-key" { name = "${terraform.workspace}-apigee-sds-app-key" } + +resource "aws_secretsmanager_secret" "external-id" { + name = "${terraform.workspace}-external-id" +} diff --git a/infrastructure/terraform/per_account/ref/parameters/provider.tf b/infrastructure/terraform/per_account/ref/parameters/provider.tf index 7d8a50a58..2f6b53001 100644 --- a/infrastructure/terraform/per_account/ref/parameters/provider.tf +++ b/infrastructure/terraform/per_account/ref/parameters/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/ref/parameters/vars.tf b/infrastructure/terraform/per_account/ref/parameters/vars.tf index 8b585d3c0..8d34250b8 100644 --- a/infrastructure/terraform/per_account/ref/parameters/vars.tf +++ b/infrastructure/terraform/per_account/ref/parameters/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "deletion_protection_enabled" { diff --git a/infrastructure/terraform/per_account/ref/provider.tf b/infrastructure/terraform/per_account/ref/provider.tf index aa11ecde0..e8b47641a 100644 --- a/infrastructure/terraform/per_account/ref/provider.tf +++ b/infrastructure/terraform/per_account/ref/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_account/ref/vars.tf b/infrastructure/terraform/per_account/ref/vars.tf index 340f2fc73..24e0fb2c7 100644 --- a/infrastructure/terraform/per_account/ref/vars.tf +++ b/infrastructure/terraform/per_account/ref/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "expiration_date" { diff --git a/infrastructure/terraform/per_workspace/provider.tf b/infrastructure/terraform/per_workspace/provider.tf index a171cdf2a..a5deb62e2 100644 --- a/infrastructure/terraform/per_workspace/provider.tf +++ b/infrastructure/terraform/per_workspace/provider.tf @@ -2,7 +2,8 @@ provider "aws" { region = local.region assume_role { - role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + role_arn = "arn:aws:iam::${var.assume_account}:role/${var.assume_role}" + external_id = var.external_id } default_tags { diff --git a/infrastructure/terraform/per_workspace/vars.tf b/infrastructure/terraform/per_workspace/vars.tf index 25eb55f12..278cc9c7e 100644 --- a/infrastructure/terraform/per_workspace/vars.tf +++ b/infrastructure/terraform/per_workspace/vars.tf @@ -8,6 +8,8 @@ variable "assume_account" { variable "assume_role" {} +variable "external_id" {} + variable "environment" {} variable "deletion_protection_enabled" { diff --git a/pyproject.toml b/pyproject.toml index f4d349d58..98c201739 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "connecting-party-manager" -version = "2025.03.05" +version = "2025.03.07" description = "Repository for the Connecting Party Manager API and related services" authors = ["NHS England"] license = "LICENSE.md" @@ -61,7 +61,7 @@ click = "^8.1.7" optional = true [tool.poetry.group.local.dependencies] -ipython = "^8.17.2" +ipython = "^9.0.1" # [tool.poetry.group.sds_update] # optional = true diff --git a/scripts/infrastructure/policies/role-trust-policy.json b/scripts/infrastructure/policies/role-trust-policy.json index b07753b6c..4bbbb8cf2 100644 --- a/scripts/infrastructure/policies/role-trust-policy.json +++ b/scripts/infrastructure/policies/role-trust-policy.json @@ -7,7 +7,11 @@ "AWS": "arn:aws:iam::${MGMT_ACCOUNT_ID}:root" }, "Action": "sts:AssumeRole", - "Condition": {} + "Condition": { + "StringEquals": { + "sts:ExternalId": "${EXTERNAL_ID}" + } + } } ] } diff --git a/scripts/infrastructure/roles.mk b/scripts/infrastructure/roles.mk index d30a90cce..e1b4dd2b9 100644 --- a/scripts/infrastructure/roles.mk +++ b/scripts/infrastructure/roles.mk @@ -8,3 +8,6 @@ manage--non-mgmt-policies: aws--login ## Create or update IAM Policies manage--non-mgmt-test-policies: aws--login ## Create or update IAM Policies @AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) AWS_SESSION_TOKEN=$(AWS_SESSION_TOKEN) bash $(PATH_TO_INFRASTRUCTURE)/roles/manage-non-mgmt-aws-support-integration-policies.sh + +manage--non-mgmt-trust-policies: aws--login ## Create or update IAM Policies + @AWS_ACCESS_KEY_ID=$(AWS_ACCESS_KEY_ID) AWS_SECRET_ACCESS_KEY=$(AWS_SECRET_ACCESS_KEY) AWS_SESSION_TOKEN=$(AWS_SESSION_TOKEN) bash $(PATH_TO_INFRASTRUCTURE)/roles/manage-non-mgmt-aws-trust-policy.sh diff --git a/scripts/infrastructure/roles/manage-non-mgmt-aws-roles.sh b/scripts/infrastructure/roles/manage-non-mgmt-aws-roles.sh index f59948894..4dd094cc5 100755 --- a/scripts/infrastructure/roles/manage-non-mgmt-aws-roles.sh +++ b/scripts/infrastructure/roles/manage-non-mgmt-aws-roles.sh @@ -32,11 +32,11 @@ else fi fi MGMT_ID_PARAMETER_STORE="nhse-cpm--${ENV}--mgmt-account-id-v1.0.0" - +EXTERNAL_ID_PARAMETER_STORE="${ENV}-external-id" if aws secretsmanager describe-secret --secret-id "$MGMT_ID_PARAMETER_STORE" --region "$AWS_REGION_NAME" &> /dev/null; then # Secret exists, retrieve its value MGMT_ACCOUNT_ID=$(aws secretsmanager get-secret-value --secret-id "$MGMT_ID_PARAMETER_STORE" --region "$AWS_REGION_NAME" --query 'SecretString' --output text) - + EXTERNAL_ID=$(aws secretsmanager get-secret-value --secret-id "$EXTERNAL_ID_PARAMETER_STORE" --region "$AWS_REGION_NAME" --query 'SecretString' --output text) # # Create the NHSDeploymentRole that will be used for deployment and CI/CD in All Deployment environments # diff --git a/scripts/infrastructure/roles/manage-non-mgmt-aws-trust-policy.sh b/scripts/infrastructure/roles/manage-non-mgmt-aws-trust-policy.sh new file mode 100755 index 000000000..38a364ae4 --- /dev/null +++ b/scripts/infrastructure/roles/manage-non-mgmt-aws-trust-policy.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +function _substitute_environment_variables() { + eval "cat << EOF +$(cat $1) +EOF" +} + +AWS_REGION_NAME="eu-west-2" +ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account) + +ENV="dev" + +# +# Check we're not running this against MGMT +# +. "./scripts/aws/helpers.sh" +if _validate_current_account "MGMT"; then + echo "Please login to non-mgmt profile before running this script" + exit 1 +else + if _validate_current_account "PROD"; then + ENV="prod" + elif _validate_current_account "INT"; then + ENV="int" + elif _validate_current_account "QA"; then + ENV="qa" + elif _validate_current_account "INT"; then + ENV="int" + elif _validate_current_account "REF"; then + ENV="ref" + fi +fi +EXTERNAL_ID_PARAMETER_STORE="${ENV}-external-id" +MGMT_ID_PARAMETER_STORE="nhse-cpm--${ENV}--mgmt-account-id-v1.0.0" + +EXTERNAL_ID=$(aws secretsmanager get-secret-value --secret-id "$EXTERNAL_ID_PARAMETER_STORE" --region "$AWS_REGION_NAME" --query 'SecretString' --output text) +MGMT_ACCOUNT_ID=$(aws secretsmanager get-secret-value --secret-id "$MGMT_ID_PARAMETER_STORE" --region "$AWS_REGION_NAME" --query 'SecretString' --output text) + +tf_assume_role_policy=$(_substitute_environment_variables ./scripts/infrastructure/policies/role-trust-policy.json) +aws iam update-assume-role-policy --role-name NHSDeploymentRole --policy-document "${tf_assume_role_policy}" +aws iam update-assume-role-policy --role-name NHSSmokeTestRole --policy-document "${tf_assume_role_policy}" + +if [ "$ENV" != "prod" ]; then + aws iam update-assume-role-policy --role-name NHSDevelopmentRole --policy-document "${tf_assume_role_policy}" + aws iam update-assume-role-policy --role-name NHSTestCIRole --policy-document "${tf_assume_role_policy}" +fi diff --git a/scripts/infrastructure/terraform/terraform-commands.sh b/scripts/infrastructure/terraform/terraform-commands.sh index 1525918f6..5e1e628f2 100644 --- a/scripts/infrastructure/terraform/terraform-commands.sh +++ b/scripts/infrastructure/terraform/terraform-commands.sh @@ -22,6 +22,7 @@ function _terraform() { account=$(_get_account_name "$AWS_ACCOUNT") || return 1 workspace=$(_get_workspace_name "$account" "$lowercase_string") || return 1 aws_account_id=$(_get_aws_account_id "$account" "$PROFILE_PREFIX" "$VERSION") || return 1 + external_id=$(_get_external_id "$account") var_file=$(_get_workspace_vars_file "$account") || return 1 scope=$(_get_terraform_scope "$TERRAFORM_SCOPE") || return 1 @@ -116,6 +117,7 @@ function _terraform_plan() { -var-file="$var_file" \ -var "assume_account=${aws_account_id}" \ -var "assume_role=${terraform_role_name}" \ + -var "external_id=${external_id}" \ -var "updated_date=${current_date}" \ -var "expiration_date=${expiration_date}" \ -var "lambdas=${lambdas}" \ @@ -128,6 +130,7 @@ function _terraform_plan() { -var-file="$var_file" \ -var "assume_account=${aws_account_id}" \ -var "assume_role=${terraform_role_name}" \ + -var "external_id=${external_id}" \ -var "updated_date=${current_date}" \ -var "expiration_date=${expiration_date}" || return 1 fi @@ -157,6 +160,7 @@ function _terraform_destroy() { -var-file="$var_file" \ -var "assume_account=${aws_account_id}" \ -var "assume_role=${terraform_role_name}" \ + -var "external_id=${external_id}" \ -var "workspace_type=${workspace_type}" \ -var "lambdas=${lambdas}" \ -var "layers=${layers}" \ diff --git a/scripts/infrastructure/terraform/terraform-utils.sh b/scripts/infrastructure/terraform/terraform-utils.sh index a485596ef..ba914afd7 100644 --- a/scripts/infrastructure/terraform/terraform-utils.sh +++ b/scripts/infrastructure/terraform/terraform-utils.sh @@ -78,6 +78,21 @@ function _get_aws_account_id() { --output text } +function _get_external_id() { + local env=$1 + + if [[ -z "$env" ]]; then + env="dev" + fi + + external_id_name="nhse-cpm--mgmt--${env}-external-id" + + aws secretsmanager get-secret-value \ + --secret-id "$external_id_name" \ + --query SecretString \ + --output text +} + function _get_workspace_vars_file() { local dir=$(pwd) local account=$1 diff --git a/src/api/createCpmProduct/tests/test_index.py b/src/api/createCpmProduct/tests/test_index.py index 92e6766c0..2ca0f2afc 100644 --- a/src/api/createCpmProduct/tests/test_index.py +++ b/src/api/createCpmProduct/tests/test_index.py @@ -62,6 +62,7 @@ def test_index(version): { "id": product["id"], "cpm_product_team_id": product["cpm_product_team_id"], + "product_team_id": product_team_payload["keys"][0]["key_value"], "name": "Foobar product", "ods_code": "F5H1R", "status": "active", diff --git a/src/api/deleteCpmProduct/tests/test_index.py b/src/api/deleteCpmProduct/tests/test_index.py index 61a15be23..d6e48d8ad 100644 --- a/src/api/deleteCpmProduct/tests/test_index.py +++ b/src/api/deleteCpmProduct/tests/test_index.py @@ -102,6 +102,7 @@ def test_index(): "name": PRODUCT_NAME, "ods_code": ODS_CODE, "cpm_product_team_id": product_team.id, + "product_team_id": CPM_PRODUCT_TEAM_NO_ID["keys"][0]["key_value"], "status": Status.INACTIVE, "keys": [], } diff --git a/src/api/tests/feature_tests/features/createCpmProduct.success.feature b/src/api/tests/feature_tests/features/createCpmProduct.success.feature index c58d26f94..c75754e85 100644 --- a/src/api/tests/feature_tests/features/createCpmProduct.success.feature +++ b/src/api/tests/feature_tests/features/createCpmProduct.success.feature @@ -20,36 +20,38 @@ Feature: Create CPM Product - success scenarios | name | My Great Product | And I note the response field "$.id" as "product_id" Then I receive a status code "201" with body - | path | value | - | id | ${ note(product_id) } | - | name | My Great Product | - | cpm_product_team_id | ${ note(product_team_id) } | - | ods_code | F5H1R | - | status | active | - | keys | [] | - | created_on | << ignore >> | - | updated_on | << ignore >> | - | deleted_on | << ignore >> | + | path | value | + | id | ${ note(product_id) } | + | name | My Great Product | + | product_team_id | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 | + | cpm_product_team_id | ${ note(product_team_id) } | + | ods_code | F5H1R | + | status | active | + | keys | [] | + | created_on | << ignore >> | + | updated_on | << ignore >> | + | deleted_on | << ignore >> | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 253 | + | Content-Length | 312 | When I make a "GET" request with "default" headers to "Product/${ note(product_id) }" Then I receive a status code "200" with body - | path | value | - | id | ${ note(product_id) } | - | name | My Great Product | - | cpm_product_team_id | ${ note(product_team_id) } | - | ods_code | F5H1R | - | status | active | - | keys | [] | - | created_on | << ignore >> | - | updated_on | << ignore >> | - | deleted_on | << ignore >> | + | path | value | + | id | ${ note(product_id) } | + | name | My Great Product | + | product_team_id | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 | + | cpm_product_team_id | ${ note(product_team_id) } | + | ods_code | F5H1R | + | status | active | + | keys | [] | + | created_on | << ignore >> | + | updated_on | << ignore >> | + | deleted_on | << ignore >> | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 253 | + | Content-Length | 312 | Examples: | product_team_id | diff --git a/src/api/tests/feature_tests/features/readCpmProduct.success.feature b/src/api/tests/feature_tests/features/readCpmProduct.success.feature index 532663447..710cbb2cb 100644 --- a/src/api/tests/feature_tests/features/readCpmProduct.success.feature +++ b/src/api/tests/feature_tests/features/readCpmProduct.success.feature @@ -20,10 +20,44 @@ Feature: Read CPM Product - success scenarios | name | My Great Product | And I note the response field "$.id" as "product_id" When I make a "GET" request with "default" headers to "Product/" + Then I receive a status code "200" with body + | path | value | + | id | | + | cpm_product_team_id | ${ note(product_team_id) } | + | product_team_id | 8babe222-5c78-42c6-8aa6-a3c69943030a | + | name | My Great Product | + | ods_code | F5H1R | + | status | active | + | keys | [] | + | created_on | << ignore >> | + | updated_on | << ignore >> | + | deleted_on | << ignore >> | + And the response headers contain: + | name | value | + | Content-Type | application/json | + | Content-Length | 312 | + + Examples: + | product_team_id | product_id | + | ${ note(product_team_id) } | ${ note(product_id) } | + | 8babe222-5c78-42c6-8aa6-a3c69943030a | ${ note(product_id) } | + + Scenario Outline: Read an existing CpmProduct without a product_team_id + Given I have already made a "POST" request with "default" headers to "ProductTeam" with body: + | path | value | + | name | My Great Product Team | + | ods_code | F5H1R | + Given I note the response field "$.id" as "product_team_id" + When I make a "POST" request with "default" headers to "ProductTeam/${ note(product_team_id) }/Product" with body: + | path | value | + | name | My Great Product | + And I note the response field "$.id" as "product_id" + When I make a "GET" request with "default" headers to "Product/" Then I receive a status code "200" with body | path | value | | id | | | cpm_product_team_id | ${ note(product_team_id) } | + | product_team_id | null | | name | My Great Product | | ods_code | F5H1R | | status | active | @@ -34,9 +68,8 @@ Feature: Read CPM Product - success scenarios And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 253 | + | Content-Length | 278 | Examples: - | product_team_id | product_id | - | ${ note(product_team_id) } | ${ note(product_id) } | - | 8babe222-5c78-42c6-8aa6-a3c69943030a | ${ note(product_id) } | + | product_team_id | product_id | + | ${ note(product_team_id) } | ${ note(product_id) } | diff --git a/src/api/tests/feature_tests/features/searchCpmProduct.success.feature b/src/api/tests/feature_tests/features/searchCpmProduct.success.feature index 694ffcb98..8cda8cbd8 100644 --- a/src/api/tests/feature_tests/features/searchCpmProduct.success.feature +++ b/src/api/tests/feature_tests/features/searchCpmProduct.success.feature @@ -38,22 +38,23 @@ Feature: Search Products - success scenarios And I note the response field "$.id" as "product_id" When I make a "GET" request with "default" headers to "Product?product_team_id=${ note(product_team_id) }" Then I receive a status code "200" with body - | path | value | - | results.0.org_code | F5H1R | - | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.id | ${ note(product_id) } | - | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.name | My Great Product | - | results.0.product_teams.0.products.0.ods_code | F5H1R | - | results.0.product_teams.0.products.0.status | active | - | results.0.product_teams.0.products.0.created_on | << ignore >> | - | results.0.product_teams.0.products.0.updated_on | null | - | results.0.product_teams.0.products.0.deleted_on | null | - | results.0.product_teams.0.products.0.keys | [] | + | path | value | + | results.0.org_code | F5H1R | + | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.id | ${ note(product_id) } | + | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.product_team_id | 808a36db-a52a-4130-b71e-d9cbcbaed15b | + | results.0.product_teams.0.products.0.name | My Great Product | + | results.0.product_teams.0.products.0.ods_code | F5H1R | + | results.0.product_teams.0.products.0.status | active | + | results.0.product_teams.0.products.0.created_on | << ignore >> | + | results.0.product_teams.0.products.0.updated_on | null | + | results.0.product_teams.0.products.0.deleted_on | null | + | results.0.product_teams.0.products.0.keys | [] | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 385 | + | Content-Length | 444 | Examples: | product_team_id | @@ -74,22 +75,23 @@ Feature: Search Products - success scenarios And I note the response field "$.id" as "product_id" When I make a "GET" request with "default" headers to "Product?organisation_code=F5H1R" Then I receive a status code "200" with body - | path | value | - | results.0.org_code | F5H1R | - | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.id | ${ note(product_id) } | - | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.name | My Great Product | - | results.0.product_teams.0.products.0.ods_code | F5H1R | - | results.0.product_teams.0.products.0.status | active | - | results.0.product_teams.0.products.0.created_on | << ignore >> | - | results.0.product_teams.0.products.0.updated_on | null | - | results.0.product_teams.0.products.0.deleted_on | null | - | results.0.product_teams.0.products.0.keys | [] | + | path | value | + | results.0.org_code | F5H1R | + | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.id | ${ note(product_id) } | + | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.product_team_id | 808a36db-a52a-4130-b71e-d9cbcbaed15b | + | results.0.product_teams.0.products.0.name | My Great Product | + | results.0.product_teams.0.products.0.ods_code | F5H1R | + | results.0.product_teams.0.products.0.status | active | + | results.0.product_teams.0.products.0.created_on | << ignore >> | + | results.0.product_teams.0.products.0.updated_on | null | + | results.0.product_teams.0.products.0.deleted_on | null | + | results.0.product_teams.0.products.0.keys | [] | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 385 | + | Content-Length | 444 | Scenario Outline: Successfully search more than one Product with product team id or alias Given I have already made a "POST" request with "default" headers to "ProductTeam" with body: @@ -113,40 +115,43 @@ Feature: Search Products - success scenarios And I note the response field "$.id" as "product_id_3" When I make a "GET" request with "default" headers to "Product?product_team_id=${ note(product_team_id) }" Then I receive a status code "200" with body where ProductTeams has a length of "1" with "3" Products each - | path | value | - | results.0.org_code | F5H1R | - | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.id | ${ note(product_id_1) } | - | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.name | My Great Product 1 | - | results.0.product_teams.0.products.0.ods_code | F5H1R | - | results.0.product_teams.0.products.0.status | active | - | results.0.product_teams.0.products.0.created_on | << ignore >> | - | results.0.product_teams.0.products.0.updated_on | null | - | results.0.product_teams.0.products.0.deleted_on | null | - | results.0.product_teams.0.products.0.keys | [] | - | results.0.product_teams.0.products.1.id | ${ note(product_id_2) } | - | results.0.product_teams.0.products.1.cpm_product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.1.name | My Great Product 2 | - | results.0.product_teams.0.products.1.ods_code | F5H1R | - | results.0.product_teams.0.products.1.status | active | - | results.0.product_teams.0.products.1.created_on | << ignore >> | - | results.0.product_teams.0.products.1.updated_on | null | - | results.0.product_teams.0.products.1.deleted_on | null | - | results.0.product_teams.0.products.1.keys | [] | - | results.0.product_teams.0.products.2.id | ${ note(product_id_3) } | - | results.0.product_teams.0.products.2.cpm_product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.2.name | My Great Product 3 | - | results.0.product_teams.0.products.2.ods_code | F5H1R | - | results.0.product_teams.0.products.2.status | active | - | results.0.product_teams.0.products.2.created_on | << ignore >> | - | results.0.product_teams.0.products.2.updated_on | null | - | results.0.product_teams.0.products.2.deleted_on | null | - | results.0.product_teams.0.products.2.keys | [] | + | path | value | + | results.0.org_code | F5H1R | + | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.id | ${ note(product_id_1) } | + | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.product_team_id | 808a36db-a52a-4130-b71e-d9cbcbaed15b | + | results.0.product_teams.0.products.0.name | My Great Product 1 | + | results.0.product_teams.0.products.0.ods_code | F5H1R | + | results.0.product_teams.0.products.0.status | active | + | results.0.product_teams.0.products.0.created_on | << ignore >> | + | results.0.product_teams.0.products.0.updated_on | null | + | results.0.product_teams.0.products.0.deleted_on | null | + | results.0.product_teams.0.products.0.keys | [] | + | results.0.product_teams.0.products.1.id | ${ note(product_id_2) } | + | results.0.product_teams.0.products.1.cpm_product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.1.product_team_id | 808a36db-a52a-4130-b71e-d9cbcbaed15b | + | results.0.product_teams.0.products.1.name | My Great Product 2 | + | results.0.product_teams.0.products.1.ods_code | F5H1R | + | results.0.product_teams.0.products.1.status | active | + | results.0.product_teams.0.products.1.created_on | << ignore >> | + | results.0.product_teams.0.products.1.updated_on | null | + | results.0.product_teams.0.products.1.deleted_on | null | + | results.0.product_teams.0.products.1.keys | [] | + | results.0.product_teams.0.products.2.id | ${ note(product_id_3) } | + | results.0.product_teams.0.products.2.cpm_product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.2.product_team_id | 808a36db-a52a-4130-b71e-d9cbcbaed15b | + | results.0.product_teams.0.products.2.name | My Great Product 3 | + | results.0.product_teams.0.products.2.ods_code | F5H1R | + | results.0.product_teams.0.products.2.status | active | + | results.0.product_teams.0.products.2.created_on | << ignore >> | + | results.0.product_teams.0.products.2.updated_on | null | + | results.0.product_teams.0.products.2.deleted_on | null | + | results.0.product_teams.0.products.2.keys | [] | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 901 | + | Content-Length | 1078 | Examples: | product_team_id | @@ -179,6 +184,7 @@ Feature: Search Products - success scenarios | results.0.product_teams.0.product_team_id | ${ note(product_team_id_1) } | | results.0.product_teams.0.products.0.id | ${ note(product_id_1) } | | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id_1) } | + | results.0.product_teams.0.products.0.product_team_id | null | | results.0.product_teams.0.products.0.name | My Great Product 1 | | results.0.product_teams.0.products.0.ods_code | F5H1R | | results.0.product_teams.0.products.0.status | active | @@ -189,6 +195,7 @@ Feature: Search Products - success scenarios | results.0.product_teams.1.product_team_id | ${ note(product_team_id_2) } | | results.0.product_teams.1.products.0.id | ${ note(product_id_2) } | | results.0.product_teams.1.products.0.cpm_product_team_id | ${ note(product_team_id_2) } | + | results.0.product_teams.1.products.0.product_team_id | null | | results.0.product_teams.1.products.0.name | My Great Product 2 | | results.0.product_teams.1.products.0.ods_code | F5H1R | | results.0.product_teams.1.products.0.status | active | @@ -199,7 +206,7 @@ Feature: Search Products - success scenarios And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 719 | + | Content-Length | 769 | Scenario: Deleted Products not returned in search Given I have already made a "POST" request with "default" headers to "ProductTeam" with body: @@ -224,28 +231,30 @@ Feature: Search Products - success scenarios And I have already made a "DELETE" request with "default" headers to "ProductTeam/${ note(product_team_id) }/Product/${ note(product_id_2) }" When I make a "GET" request with "default" headers to "Product?product_team_id=${ note(product_team_id) }" Then I receive a status code "200" with body where ProductTeams has a length of "1" with "2" Products each - | path | value | - | results.0.org_code | F5H1R | - | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.id | ${ note(product_id_1) } | - | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.0.name | My Great Product 1 | - | results.0.product_teams.0.products.0.ods_code | F5H1R | - | results.0.product_teams.0.products.0.status | active | - | results.0.product_teams.0.products.0.created_on | << ignore >> | - | results.0.product_teams.0.products.0.updated_on | null | - | results.0.product_teams.0.products.0.deleted_on | null | - | results.0.product_teams.0.products.0.keys | [] | - | results.0.product_teams.0.products.1.id | ${ note(product_id_3) } | - | results.0.product_teams.0.products.1.cpm_product_team_id | ${ note(product_team_id) } | - | results.0.product_teams.0.products.1.name | My Great Product 3 | - | results.0.product_teams.0.products.1.ods_code | F5H1R | - | results.0.product_teams.0.products.1.status | active | - | results.0.product_teams.0.products.1.created_on | << ignore >> | - | results.0.product_teams.0.products.1.updated_on | null | - | results.0.product_teams.0.products.1.deleted_on | null | - | results.0.product_teams.0.products.1.keys | [] | + | path | value | + | results.0.org_code | F5H1R | + | results.0.product_teams.0.product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.id | ${ note(product_id_1) } | + | results.0.product_teams.0.products.0.cpm_product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.0.product_team_id | 808a36db-a52a-4130-b71e-d9cbcbaed15b | + | results.0.product_teams.0.products.0.name | My Great Product 1 | + | results.0.product_teams.0.products.0.ods_code | F5H1R | + | results.0.product_teams.0.products.0.status | active | + | results.0.product_teams.0.products.0.created_on | << ignore >> | + | results.0.product_teams.0.products.0.updated_on | null | + | results.0.product_teams.0.products.0.deleted_on | null | + | results.0.product_teams.0.products.0.keys | [] | + | results.0.product_teams.0.products.1.id | ${ note(product_id_3) } | + | results.0.product_teams.0.products.1.cpm_product_team_id | ${ note(product_team_id) } | + | results.0.product_teams.0.products.1.product_team_id | 808a36db-a52a-4130-b71e-d9cbcbaed15b | + | results.0.product_teams.0.products.1.name | My Great Product 3 | + | results.0.product_teams.0.products.1.ods_code | F5H1R | + | results.0.product_teams.0.products.1.status | active | + | results.0.product_teams.0.products.1.created_on | << ignore >> | + | results.0.product_teams.0.products.1.updated_on | null | + | results.0.product_teams.0.products.1.deleted_on | null | + | results.0.product_teams.0.products.1.keys | [] | And the response headers contain: | name | value | | Content-Type | application/json | - | Content-Length | 644 | + | Content-Length | 762 | diff --git a/src/layers/domain/core/cpm_product/v1.py b/src/layers/domain/core/cpm_product/v1.py index a20bb873c..3c68ecf64 100644 --- a/src/layers/domain/core/cpm_product/v1.py +++ b/src/layers/domain/core/cpm_product/v1.py @@ -16,6 +16,7 @@ class CpmProductCreatedEvent(Event): id: str cpm_product_team_id: str + product_team_id: str name: str ods_code: str status: Status @@ -29,6 +30,7 @@ class CpmProductKeyAddedEvent(Event): new_key: dict id: str cpm_product_team_id: str + product_team_id: str name: str ods_code: str status: Status @@ -42,6 +44,7 @@ class CpmProductKeyAddedEvent(Event): class CpmProductDeletedEvent(Event): id: str cpm_product_team_id: str + product_team_id: str name: str ods_code: str status: Status @@ -58,6 +61,7 @@ class CpmProduct(AggregateRoot): id: ProductId = Field(default_factory=ProductId.create) cpm_product_team_id: str = Field(...) + product_team_id: str = Field(default=None) name: str = Field(regex=CPM_PRODUCT_NAME_REGEX, min_length=1) ods_code: str status: Status = Status.ACTIVE diff --git a/src/layers/domain/core/product_team/v1.py b/src/layers/domain/core/product_team/v1.py index 86584e3d7..7f28f9257 100644 --- a/src/layers/domain/core/product_team/v1.py +++ b/src/layers/domain/core/product_team/v1.py @@ -61,8 +61,13 @@ def set_id(cls, values): def create_cpm_product(self, name: str, product_id: str = None) -> CpmProduct: extra_kwargs = {"id": product_id} if product_id is not None else {} + product_team_id = next( + (key.key_value for key in self.keys if key.key_type == "product_team_id"), + None, + ) product = CpmProduct( cpm_product_team_id=self.id, + product_team_id=product_team_id, name=name, ods_code=self.ods_code, **extra_kwargs diff --git a/src/test_helpers/aws_session.py b/src/test_helpers/aws_session.py index d76bf37ff..73968b013 100644 --- a/src/test_helpers/aws_session.py +++ b/src/test_helpers/aws_session.py @@ -10,6 +10,7 @@ DEFAULT_WORKSPACE = "dev" SECRET_ID = "nhse-cpm--mgmt--{env}-account-id-v1.0.0" +EXTERNAL_ID = "nhse-cpm--mgmt--{env}-external-id" def _aws_account_id_from_secret(env: str): @@ -18,12 +19,20 @@ def _aws_account_id_from_secret(env: str): return response["SecretString"] -def _get_access_token(role_name: str, account_id: str = None): +def _aws_external_id_from_secret(env: str): + client = boto3.client("secretsmanager") + response = client.get_secret_value(SecretId=EXTERNAL_ID.format(env=env)) + return response["SecretString"] + + +def _get_access_token(role_name: str, env: str, account_id: str = None): sts_client = boto3.client("sts") current_time = datetime.now(timezone.utc).timestamp() + external_id = _aws_external_id_from_secret(env=env) response = sts_client.assume_role( RoleArn=f"arn:aws:iam::{account_id}:role/{role_name}", RoleSessionName=f"role--{current_time}", + ExternalId=external_id, ) access_key_id = response["Credentials"]["AccessKeyId"] @@ -37,7 +46,7 @@ def aws_session_env_vars(role_name: str) -> boto3.Session: env = os.environ.get("ACCOUNT") or read_terraform_output("environment.value") account_id = _aws_account_id_from_secret(env=env) access_key_id, secret_access_key, session_token = _get_access_token( - account_id=account_id, role_name=role_name + account_id=account_id, role_name=role_name, env=env ) return { "AWS_ACCESS_KEY_ID": access_key_id,