diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fddbbc6f48..6afa43a77b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,10 +10,12 @@ updates: # Python package update PRs - package-ecosystem: pip # This will update 'pyproject.toml' directory: "/" - schedule: - interval: weekly + ignore: + - dependency-name: "psycopg" # 3.1.19 is the latest version to support on older MacOS versions groups: production-dependencies: dependency-type: "production" development-dependencies: dependency-type: "development" + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index 379a4e5be1..889c80a9b4 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ expanded.yaml # ruff cache .ruff_cache + +**/venv diff --git a/.hatch/requirements-test.txt b/.hatch/requirements-test.txt index ce51600060..8e331e9c35 100644 --- a/.hatch/requirements-test.txt +++ b/.hatch/requirements-test.txt @@ -1,7 +1,7 @@ # # This file is autogenerated by hatch-pip-compile with Python 3.12 # -# [constraints] .hatch/requirements.txt (SHA256: 02ec09f022011bea4adeff6755ed03b7c3c1c847be7bcff73b746968aa5579a0) +# [constraints] .hatch/requirements.txt (SHA256: e9ada52b8f6ba0b8f072b9c1a47b577eeb19f863ee4e75afc8a24ad948bf35eb) # # - appdirs==1.4.4 # - azure-core==1.31.0 @@ -23,7 +23,7 @@ # - chevron==0.14.0 # - cryptography==43.0.3 # - fqdn==1.5.1 -# - psycopg[binary]==3.2.3 +# - psycopg[binary]==3.1.19 # - pulumi-azure-native==2.68.0 # - pulumi-azuread==6.0.1 # - pulumi-random==4.16.7 @@ -287,11 +287,11 @@ protobuf==4.25.5 # via # -c .hatch/requirements.txt # pulumi -psycopg==3.2.3 +psycopg==3.1.19 # via # -c .hatch/requirements.txt # hatch.envs.test -psycopg-binary==3.2.3 +psycopg-binary==3.1.19 # via # -c .hatch/requirements.txt # psycopg diff --git a/.hatch/requirements.txt b/.hatch/requirements.txt index 07020f85d4..7354e9da99 100644 --- a/.hatch/requirements.txt +++ b/.hatch/requirements.txt @@ -21,7 +21,7 @@ # - chevron==0.14.0 # - cryptography==43.0.3 # - fqdn==1.5.1 -# - psycopg[binary]==3.2.3 +# - psycopg[binary]==3.1.19 # - pulumi-azure-native==2.68.0 # - pulumi-azuread==6.0.1 # - pulumi-random==4.16.7 @@ -188,9 +188,9 @@ portalocker==2.10.1 # via msal-extensions protobuf==4.25.5 # via pulumi -psycopg==3.2.3 +psycopg==3.1.19 # via hatch.envs.default -psycopg-binary==3.2.3 +psycopg-binary==3.1.19 # via psycopg pulumi==3.137.0 # via diff --git a/SECURITY.md b/SECURITY.md index c045852320..db056c976b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,8 +7,8 @@ All organisations using an earlier version in production should update to the la | Version | Supported | | --------------------------------------------------------------------------------------- | ------------------ | -| [5.0.0](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v5.0.0) | :white_check_mark: | -| < 5.0.0 | :x: | +| [5.0.1](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v5.0.1) | :white_check_mark: | +| < 5.0.1 | :x: | ## Reporting a Vulnerability diff --git a/data_safe_haven/commands/shm.py b/data_safe_haven/commands/shm.py index b6694c7daa..522609b8ff 100644 --- a/data_safe_haven/commands/shm.py +++ b/data_safe_haven/commands/shm.py @@ -41,7 +41,7 @@ def deploy( ), ] = None, ) -> None: - """Deploy a Safe Haven Management environment.""" + """Deploy a Safe Haven Management environment using the current context.""" logger = get_logger() # Load selected context @@ -126,7 +126,7 @@ def deploy( @shm_command_group.command() def teardown() -> None: - """Tear down a deployed a Safe Haven Management environment.""" + """Tear down a deployed Safe Haven Management environment.""" logger = get_logger() try: context = ContextManager.from_file().assert_context() diff --git a/data_safe_haven/commands/sre.py b/data_safe_haven/commands/sre.py index d1f32ea278..f03f0cc53e 100644 --- a/data_safe_haven/commands/sre.py +++ b/data_safe_haven/commands/sre.py @@ -92,7 +92,7 @@ def deploy( ) logger.info( f"SRE will be deployed to subscription '[green]{sre_subscription_name}[/]'" - f" ('[bold]{sre_config.azure.subscription_id}[/]')" + f" ({sre_config.azure.subscription_id})" ) # Set Entra options application = graph_api.get_application_by_name(context.entra_application_name) @@ -136,7 +136,13 @@ def deploy( replace=True, ) logger.info(f"SRE will be registered in SHM '[green]{shm_config.shm.fqdn}[/]'") - logger.info(f"SHM subscription '[green]{shm_config.azure.subscription_id}[/]'") + shm_subscription_name = azure_sdk.get_subscription_name( + shm_config.azure.subscription_id + ) + logger.info( + f"SHM is deployed to subscription '[green]{shm_subscription_name}[/]'" + f" ({shm_config.azure.subscription_id})" + ) # Deploy Azure infrastructure with Pulumi try: diff --git a/data_safe_haven/config/context.py b/data_safe_haven/config/context.py index cadd875260..9d648c6bc8 100644 --- a/data_safe_haven/config/context.py +++ b/data_safe_haven/config/context.py @@ -35,7 +35,7 @@ class Context(ContextBase, BaseModel, validate_assignment=True): @property def entra_application_name(self) -> str: - return f"Data Safe Haven ({self.description}) Pulumi Service Principal" + return f"Data Safe Haven ({self.name}) Pulumi Service Principal" @property def entra_application_secret(self) -> str: diff --git a/data_safe_haven/external/api/graph_api.py b/data_safe_haven/external/api/graph_api.py index 20201a4919..66cf139a65 100644 --- a/data_safe_haven/external/api/graph_api.py +++ b/data_safe_haven/external/api/graph_api.py @@ -1035,7 +1035,7 @@ def verify_custom_domain( # Check whether all expected nameservers are active with suppress(resolver.NXDOMAIN): self.logger.debug( - f"Checking [green]{domain_name}[/] domain registration status ..." + f"Checking [green]{domain_name}[/] DNS delegation." ) active_nameservers = [ str(ns) for ns in iter(resolver.resolve(domain_name, "NS")) @@ -1045,23 +1045,35 @@ def verify_custom_domain( for nameserver in expected_nameservers ): self.logger.info( - f"Verified that [green]{domain_name}[/] is registered as a custom Entra ID domain." + f"[green]{domain_name}[/] DNS has been delegated correctly." ) break self.logger.warning( - f"Domain [green]{domain_name}[/] is not currently registered as a custom Entra ID domain." + f"[green]{domain_name}[/] DNS is not delegated correctly." ) # Prompt user to set domain delegation manually - docs_link = "https://learn.microsoft.com/en-us/azure/dns/dns-delegate-domain-azure-dns#delegate-the-domain" self.logger.info( - f"To proceed you will need to delegate [green]{domain_name}[/] to Azure ({docs_link})" + f"To proceed you will need to delegate [green]{domain_name}[/] to specific Azure nameservers" ) - ns_list = ", ".join([f"[green]{n}[/]" for n in expected_nameservers]) + domain_parent = ".".join(domain_name.split(".")[1:]) self.logger.info( - f"You will need to create NS records pointing to: {ns_list}" + f"Create {len(expected_nameservers)} [green]NS[/] records for [green]{domain_name}[/] (for example in the zone of {domain_parent})" + ) + console.tabulate( + header=["domain", "record type", "value"], + rows=[ + [domain_name, "NS", nameserver] + for nameserver in expected_nameservers + ], + ) + docs_link = ( + "https://www.cloudflare.com/learning/dns/dns-records/dns-ns-record/" + ) + self.logger.info( + f"You can learn more about NS records here: {docs_link}" ) if not console.confirm( - f"Are you ready to check whether [green]{domain_name}[/] has been delegated to Azure?", + f"Are you ready to check whether [green]{domain_name}[/] has been delegated to the correct Azure nameservers?", default_to_yes=True, ): self.logger.error("User terminated check for domain delegation.") diff --git a/data_safe_haven/external/interface/pulumi_account.py b/data_safe_haven/external/interface/pulumi_account.py index 4db66c52c8..0a240749f4 100644 --- a/data_safe_haven/external/interface/pulumi_account.py +++ b/data_safe_haven/external/interface/pulumi_account.py @@ -1,5 +1,6 @@ """Manage Pulumi accounts""" +import sys from shutil import which from typing import Any @@ -39,5 +40,6 @@ def env(self) -> dict[str, Any]: "AZURE_STORAGE_ACCOUNT": self.storage_account_name, "AZURE_STORAGE_KEY": str(storage_account_keys[0].value), "AZURE_KEYVAULT_AUTH_VIA_CLI": "true", + "PULUMI_PYTHON_CMD": sys.executable, } return self._env diff --git a/data_safe_haven/infrastructure/programs/imperative_shm.py b/data_safe_haven/infrastructure/programs/imperative_shm.py index 73893bff61..9b748bbdd1 100644 --- a/data_safe_haven/infrastructure/programs/imperative_shm.py +++ b/data_safe_haven/infrastructure/programs/imperative_shm.py @@ -154,14 +154,13 @@ def deploy(self) -> None: "signInAudience": "AzureADMyOrg", }, ) - # Ensure that the application secret exists - if not self.context.entra_application_secret: - self.context.entra_application_secret = ( - graph_api.create_application_secret( - self.context.entra_application_name, - self.context.entra_application_secret_name, - ) - ) + # Always recreate the application secret. + # Otherwise the one in the key vault will be used which might be out of date + # An SRE deployment will read from the keyvault, and get the latest version + self.context.entra_application_secret = graph_api.create_application_secret( + self.context.entra_application_name, + self.context.entra_application_secret_name, + ) except DataSafeHavenMicrosoftGraphError as exc: msg = "Failed to create deployment application in Entra ID." raise DataSafeHavenAzureError(msg) from exc diff --git a/data_safe_haven/infrastructure/project_manager.py b/data_safe_haven/infrastructure/project_manager.py index dcb3941af2..a6d5af805b 100644 --- a/data_safe_haven/infrastructure/project_manager.py +++ b/data_safe_haven/infrastructure/project_manager.py @@ -160,8 +160,8 @@ def apply_config_options(self) -> None: else: self.ensure_config(name, value, secret=is_secret) self._options = {} - except Exception as exc: - msg = "Applying Pulumi configuration options failed.." + except DataSafeHavenError as exc: + msg = "Applying Pulumi configuration options failed." raise DataSafeHavenPulumiError(msg) from exc def cancel(self) -> None: @@ -286,10 +286,26 @@ def destroy(self) -> None: raise DataSafeHavenPulumiError(msg) from exc def ensure_config(self, name: str, value: str, *, secret: bool) -> None: - """Ensure that config values have been set, setting them if they do not exist""" + """ + Ensure that config values have been set. + + Values will be set if they do not exist. + + If the value is already set and does not match the `value` argument, + `DataSafeHavenPulumiError` will be raised. + """ try: - self.stack.get_config(name) + # Check whether a value is already set for this parameter + existing_value = self.stack.get_config(name).value + # ... if it is, ensure it is consistent with the incoming value + if existing_value != value: + msg = ( + f"Unchangeable configuration option '{name}' not consistent, " + f"your configuration: '{value}', Pulumi workspace: '{existing_value}'." + ) + raise DataSafeHavenPulumiError(msg) except automation.CommandError: + # Set value if it does not already exist self.set_config(name, value, secret=secret) def evaluate(self, result: str) -> None: diff --git a/data_safe_haven/version.py b/data_safe_haven/version.py index bfb9e4b4b8..0513a64c8f 100644 --- a/data_safe_haven/version.py +++ b/data_safe_haven/version.py @@ -1,2 +1,2 @@ -__version__ = "5.0.0" +__version__ = "5.0.1" __version_info__ = tuple(__version__.split(".")) diff --git a/docs/source/deployment/configure_entra_id.md b/docs/source/deployment/configure_entra_id.md index 4be5339895..ea1cca6b7e 100644 --- a/docs/source/deployment/configure_entra_id.md +++ b/docs/source/deployment/configure_entra_id.md @@ -36,17 +36,18 @@ Follow the instructions [here](https://learn.microsoft.com/en-us/entra/fundament Use the following settings: - **Basics** tab: - - **User principal name:** entra.admin._FIRST_NAME_._LAST_NAME_ + - **User principal name:** entra.admin._first_name_._last_name_ - If you have a choice of domains use _YOUR_ORGANISATION_.onmicrosoft.com, which will create a clearer separation between administrators and users - - **Display name:** Entra Admin - _FIRST_NAME_ _LAST_NAME_ + - **Display name:** Entra Admin - _first_name_ _last_name_ - **Other fields:** leave them with their default values - **Properties** tab: - **Usage location:** set to the country being used for this deployment -- **Assigments** tab: +- **Assignments** tab: - Click the **{guilabel}`+ Add role`** button - Search for **Global Administrator**, and check the box - Click the **{guilabel}`Select`** button +Click the **{guilabel}`Review + Create`** button ::: ## Register allowed authentication methods diff --git a/docs/source/deployment/deploy_shm.md b/docs/source/deployment/deploy_shm.md index 098784004c..b26d451bfb 100644 --- a/docs/source/deployment/deploy_shm.md +++ b/docs/source/deployment/deploy_shm.md @@ -17,7 +17,7 @@ However, you may choose to use multiple SHMs if, for example, you want to separa ## Requirements - A [Microsoft Entra](https://learn.microsoft.com/en-us/entra/fundamentals/) tenant for managing your users - - An account with [Global Administrator](https://learn.microsoft.com/en-us/entra/global-secure-access/reference-role-based-permissions#global-administrator) privileges on this tenant + - An account with [Global Administrator](https://learn.microsoft.com/en-us/entra/global-secure-access/reference-role-based-permissions#global-administrator) privileges on the tenant that you set up in the {ref}`configure_entra_id` step. - An Azure subscription where you will deploy your infrastructure - An account with at least [Contributor](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles/general#contributor) permissions on this subscription @@ -26,7 +26,7 @@ However, you may choose to use multiple SHMs if, for example, you want to separa Before deploying the Safe Haven Management (SHM) infrastructure you need to decide on a few parameters: **entra_tenant_id** -: Tenant ID for the Entra ID used to manage TRE users +: Tenant ID for the Entra tenant you will be using to manage the TRE users :::{admonition} How to find your Microsoft Entra Tenant ID :class: dropdown hint diff --git a/docs/source/deployment/index.md b/docs/source/deployment/index.md index 38fc1cdf64..435d32ceb5 100644 --- a/docs/source/deployment/index.md +++ b/docs/source/deployment/index.md @@ -21,6 +21,7 @@ Deploying an instance of the Data Safe Haven involves the following steps: Install the following requirements before starting +- [Python 3.12](https://wiki.python.org/moin/BeginnersGuide/Download) - [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) - [pipx](https://pipx.pypa.io/stable/installation/) - [Pulumi](https://www.pulumi.com/docs/get-started/install/) diff --git a/docs/source/management/egress_token_read_only.png b/docs/source/management/egress_token_read_only.png new file mode 100644 index 0000000000..a28ca2e99d Binary files /dev/null and b/docs/source/management/egress_token_read_only.png differ diff --git a/docs/source/management/index.md b/docs/source/management/index.md index e4f8406ab7..e9f49a5733 100644 --- a/docs/source/management/index.md +++ b/docs/source/management/index.md @@ -1,6 +1,8 @@ # Management -## Add users to the Data Safe Haven +## Managing users + +### Add users to the Data Safe Haven :::{important} You will need a full name, phone number, email address and country for each user. @@ -27,7 +29,7 @@ Grace;Hopper;+18005550100;grace@nasa.gov;US $ dsh users add PATH_TO_MY_CSV_FILE ``` -## Listing available users +### List available users - You can do this from the [Microsoft Entra admin centre](https://entra.microsoft.com/) @@ -54,7 +56,7 @@ $ dsh users add PATH_TO_MY_CSV_FILE └──────────────────────────────┴──────────┴───────────────────┘ ``` -## Assign existing users to an SRE +### Assign existing users to an SRE 1. You can do this directly in your Entra tenant by adding them to the **Data Safe Haven SRE _YOUR\_SRE\_NAME_ Users** group, following the instructions [here](https://learn.microsoft.com/en-us/entra/fundamentals/groups-view-azure-portal#add-a-group-member). @@ -70,7 +72,7 @@ $ dsh users add PATH_TO_MY_CSV_FILE Do not include the Entra ID domain part of the username, just the part before the @. ::: -## Manually register users for self-service password reset +### Manually register users for self-service password reset :::{tip} Users created via the `dsh users` command line tool will be automatically registered for SSPR. @@ -87,7 +89,9 @@ If you have manually created a user and want to enable SSPR, do the following - **Email:** enter the user's email address here - Click the **{guilabel}`Save`** icon in the top panel -## Listing available SRE configurations and deployment status +## Managing SREs + +### List available SRE configurations and deployment status - Run the following if you want to check what SRE configurations are available in the current context, and whether those SREs are deployed @@ -108,7 +112,7 @@ Available SRE configurations for context 'green': └──────────────┴──────────┘ ``` -## Removing a deployed Data Safe Haven +### Remove a deployed Data Safe Haven - Run the following if you want to teardown a deployed SRE: @@ -116,8 +120,96 @@ Available SRE configurations for context 'green': $ dsh sre teardown YOUR_SRE_NAME ``` +::::{admonition} Tearing down an SRE is destructive and irreversible +:class: danger +Running `dsh sre teardown` will destroy **all** resources deployed within the SRE. +Ensure that any desired outputs have been extracted before deleting the SRE. +**All** data remaining on the SRE will be deleted. +The user groups for the SRE on Microsoft Entra ID will also be deleted. +:::: + - Run the following if you want to teardown the deployed SHM: ```{code} shell $ dsh shm teardown ``` + +::::{admonition} Tearing down an SHM +:class: warning +Tearing down the SHM permanently deletes **all** remotely stored configuration and state data. +Tearing down the SHM also renders the SREs inaccessible to users and prevents them from being fully managed using the CLI. +All SREs associated with the SHM should be torn down before the SHM is torn down. +:::: + +## Managing data ingress and egress + +### Data Ingress + +It is the {ref}`role_data_provider_representative`'s responsibility to upload the data required by the safe haven. + +The following steps show how to generate a temporary, write-only upload token that can be securely sent to the {ref}`role_data_provider_representative`, enabling them to upload the data: + +- In the Azure portal select **Subscriptions** then navigate to the subscription containing the relevant SHM +- Search for the resource group: `shm--sre--rg`, then click through to the storage account ending with `sensitivedata` +- Browse to **{menuselection}`Settings --> Networking`** and ensure that the data provider's IP address is one of those allowed under the **Firewall** header + - If it is not listed, modify and reupload the SRE configuration and redeploy the SRE using the `dsh` CLI, as per {ref}`deploy_sre` +- Browse to **{menuselection}`Data storage --> Containers`** from the menu on the left hand side +- Click **ingress** +- Browse to **{menuselection}`Settings --> Shared access tokens`** and do the following: + - Under **Signing method**, select **User delegation key** + - Under **Permissions**, check these boxes: + - **Write** + - **List** + - Set a 24 hour time window in the **Start and expiry date/time** (or an appropriate length of time) + - Leave everything else as default and click **{guilabel}`Generate SAS token and URL`** + - Copy the **Blob SAS URL** + + ```{image} ingress_token_write_only.png + :alt: write-only SAS token + :align: center + ``` + +- Send the **Blob SAS URL** to the data provider through a secure channel +- The data provider should now be able to upload data +- Validate successful data ingress + - Browse to **{menuselection}`Data storage --> Containers`** (in the middle of the page) + - Select the **ingress** container and ensure that the uploaded files are present + +### Data egress + +```{important} +Assessment of output must be completed **before** an egress link is created. +Outputs are potentially sensitive, and so an appropriate process must be applied to ensure that they are suitable for egress. +``` + +The {ref}`role_system_manager` creates a time-limited and IP restricted link to remove data from the environment. + +- In the Azure portal select **Subscriptions** then navigate to the subscription containing the relevant SHM +- Search for the resource group: `shm--sre--rg`, then click through to the storage account ending with `sensitivedata` +- Browse to **{menuselection}`Settings --> Networking`** and check the list of pre-approved IP addresses allowed under the **Firewall** header + - Ensure that the IP address of the person to receive the outputs is listed + - If it is not listed, modify and reupload the SRE configuration and redeploy the SRE using the `dsh` CLI, as per {ref}`deploy_sre` +- Browse to **{menuselection}`Data storage --> Containers`** +- Select the **egress** container +- Browse to **{menuselection}`Settings --> Shared access tokens`** and do the following: + - Under **Signing method**, select **User delegation key** + - Under **Permissions**, check these boxes: + - **Read** + - **List** + - Set a time window in the **Start and expiry date/time** that gives enough time for the person who will perform the secure egress download to do so + - Leave everything else as default and press **{guilabel}`Generate SAS token and URL`** + - Copy the **Blob SAS URL** + + ```{image} egress_token_read_only.png + :alt: Read-only SAS token + :align: center + ``` + +- Send the **Blob SAS URL** to the relevant person through a secure channel +- The appropriate person should now be able to download data + +### The output volume + +Once you have set up the egress connection in Azure Storage Explorer, you should be able to view data from the **output volume**, a read-write area intended for the extraction of results, such as figures for publication. +On the workspaces, this volume is `/mnt/output` and is shared between all workspaces in an SRE. +For more information on shared SRE storage volumes, consult the {ref}`Safe Haven User Guide `. diff --git a/docs/source/management/ingress_token_write_only.png b/docs/source/management/ingress_token_write_only.png new file mode 100644 index 0000000000..34829ee4fa Binary files /dev/null and b/docs/source/management/ingress_token_write_only.png differ diff --git a/docs/source/roles/data_provider_representative/data_ingress.md b/docs/source/roles/data_provider_representative/data_ingress.md index 659ee706ad..cb4e12faa4 100644 --- a/docs/source/roles/data_provider_representative/data_ingress.md +++ b/docs/source/roles/data_provider_representative/data_ingress.md @@ -5,9 +5,22 @@ The **Dataset Provider Representative** plays an important role in data ingress. As well as being involved in agreeing an appropriate security tier for a project, they may also prepare the data to be uploaded. -## Preparing data +## Bringing data into the environment + +Talk to your {ref}`role_system_manager` to discuss possible methods of bringing data into the environments. +It may be convenient to use [Azure Storage Explorer](https://azure.microsoft.com/en-us/products/storage/storage-explorer/). +In this case you will not need log-in credentials, as your {ref}`role_system_manager` can provide a short-lived secure access token which will let you upload data. + +```{tip} +You may want to keep the following considerations in mind when transferring data in order to reduce the chance of a data breach. + +- Use of short-lived access tokens limits the time within which an attacker can operate. +- Letting your {ref}`role_system_manager` know a fixed IP address you will be connecting from (_e.g._ a corporate VPN) limits the places an attacker can operate from. +- Communicating with your {ref}`role_system_manager` through a secure out-of-band channel (_e.g._ encrypted email) reduces the chances that an attacker can intercept or alter your messages in transit. + +``` -This section has some recommendations for preparing input data for the Data Safe Haven. +## Preparing input data for the Data Safe Haven ### Avoid archives @@ -86,16 +99,3 @@ md5sum -c hashes.txt | grep FAILED ``` To use the `sha256` algorithm, replace `md5sum` with `sha256` in the above commands. - -## Bringing data into the environment - -Talk to your {ref}`role_system_manager` to discuss possible methods of bringing data into the environments. -It may be convenient to use [Azure Storage Explorer](https://azure.microsoft.com/en-us/products/storage/storage-explorer/). -In this case you will not need log-in credentials, as your {ref}`role_system_manager` can provide a short-lived secure access token which will let you upload data. - -```{tip} -You may want to keep the following considerations in mind when transferring data in order to reduce the chance of a data breach -- use of short-lived access tokens limits the time within which an attacker can operate -- letting your {ref}`role_system_manager` know a fixed IP address you will be connecting from (eg. a corporate VPN) limits the places an attacker can operate from -- communicating with your {ref}`role_system_manager` through a secure out-of-band channel (eg. encrypted email) reduces the chances that an attacker can intercept or alter your messages in transit -``` diff --git a/pyproject.toml b/pyproject.toml index cd7dd999b0..97461b4d32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "chevron==0.14.0", "cryptography==43.0.3", "fqdn==1.5.1", - "psycopg[binary]==3.2.3", + "psycopg[binary]==3.1.19", # needed for installation on older MacOS versions "pulumi-azure-native==2.68.0", "pulumi-azuread==6.0.1", "pulumi-random==4.16.7", diff --git a/tests/commands/test_sre.py b/tests/commands/test_sre.py index f8818d20cc..6c4a13f545 100644 --- a/tests/commands/test_sre.py +++ b/tests/commands/test_sre.py @@ -43,7 +43,7 @@ def test_no_application( result = runner.invoke(sre_command_group, ["deploy", "sandbox"]) assert result.exit_code == 1 assert ( - "No Entra application 'Data Safe Haven (Acme Deployment) Pulumi Service Principal' was found." + "No Entra application 'Data Safe Haven (acmedeployment) Pulumi Service Principal' was found." in caplog.text ) assert "Please redeploy your SHM." in caplog.text diff --git a/tests/config/test_context_manager.py b/tests/config/test_context_manager.py index 6167fb2096..c4a6b5e39c 100644 --- a/tests/config/test_context_manager.py +++ b/tests/config/test_context_manager.py @@ -34,7 +34,7 @@ def test_invalid_subscription_name(self, context_dict): def test_entra_application_name(self, context: Context) -> None: assert ( context.entra_application_name - == "Data Safe Haven (Acme Deployment) Pulumi Service Principal" + == "Data Safe Haven (acmedeployment) Pulumi Service Principal" ) def test_entra_application_secret(self, context: Context, mocker) -> None: diff --git a/tests/infrastructure/test_project_manager.py b/tests/infrastructure/test_project_manager.py index 259c5f1b37..8c74a83808 100644 --- a/tests/infrastructure/test_project_manager.py +++ b/tests/infrastructure/test_project_manager.py @@ -49,6 +49,22 @@ def test_cleanup( ) assert "Purged Azure Key Vault shmacmedsresandbosecrets." in stdout + def test_ensure_config(self, sre_project_manager): + sre_project_manager.ensure_config( + "azure-native:location", "uksouth", secret=False + ) + sre_project_manager.ensure_config("data-safe-haven:variable", "8", secret=False) + + def test_ensure_config_exception(self, sre_project_manager): + + with raises( + DataSafeHavenPulumiError, + match=r"Unchangeable configuration option 'azure-native:location'.*your configuration: 'ukwest', Pulumi workspace: 'uksouth'", + ): + sre_project_manager.ensure_config( + "azure-native:location", "ukwest", secret=False + ) + def test_new_project( self, context_no_secrets,