WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit 4a237a9

Browse files
authored
Merge pull request #2384 from alan-turing-institute/release-v5.3.1
Release v5.3.1
2 parents de563f8 + 70ad75a commit 4a237a9

File tree

8 files changed

+64
-37
lines changed

8 files changed

+64
-37
lines changed

SECURITY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ All organisations using an earlier version in production should update to the la
77

88
| Version | Supported |
99
| ------------------------------------------------------------------------------------- | ------------------ |
10-
| [5.3.0](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v5.2.1) | :white_check_mark: |
11-
| < 5.3.0 | :x: |
10+
| [5.3.1](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v5.2.1) | :white_check_mark: |
11+
| < 5.3.1 | :x: |
1212

1313
## Reporting a Vulnerability
1414

VERSIONING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Additionally, a production instance of DSH is maintained for use by research pro
8080
| 2023 | [v4.0.3](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v4.0.3) |
8181
| 2023–2024 | [v4.1.0](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v4.1.0) |
8282
| 2024 | [v4.2.0](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v4.2.0) |
83+
| 2025 | [v5.3.0](https://github.com/alan-turing-institute/data-safe-haven/releases/tag/v5.3.0) |
8384

8485
## Versions that have undergone formal security evaluation
8586

data_safe_haven/external/api/azure_sdk.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -641,10 +641,10 @@ def get_keyvault_certificate(
641641
"""Read a certificate from the KeyVault
642642
643643
Returns:
644-
KeyVaultCertificate: The certificate
644+
The KeyVaultCertificate
645645
646646
Raises:
647-
DataSafeHavenAzureError if the secret could not be read
647+
DataSafeHavenAzureError if the certificate could not be read
648648
"""
649649
# Connect to Azure clients
650650
certificate_client = CertificateClient(
@@ -827,7 +827,9 @@ def import_keyvault_certificate(
827827
)
828828
break
829829
except ResourceExistsError:
830-
# Purge any existing deleted certificate with the same name
830+
# Delete any certificate with the same name
831+
self.remove_keyvault_certificate(certificate_name, key_vault_name)
832+
# Purge any existing deleted certificate
831833
self.purge_keyvault_certificate(certificate_name, key_vault_name)
832834
self.logger.info(
833835
f"Imported certificate [green]{certificate_name}[/].",
@@ -1094,8 +1096,8 @@ def remove_keyvault_certificate(
10941096
self.logger.debug(
10951097
f"Waiting for deletion to complete for certificate [green]{certificate_name}[/]..."
10961098
)
1097-
while True:
1098-
# Keep polling until deleted certificate is available
1099+
# Keep polling until deleted certificate is available or 2 minutes have elapsed
1100+
for _ in range(12):
10991101
with suppress(ResourceNotFoundError):
11001102
if certificate_client.get_deleted_certificate(certificate_name):
11011103
break

data_safe_haven/infrastructure/components/dynamic/dsh_resource_provider.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ def create(self, props: dict[str, Any]) -> CreateResult:
8484
8585
Returns:
8686
CreateResult: a unique ID for this object plus a set of output properties
87+
88+
Raises:
89+
An appropriate DataSafeHavenError if the resource could not be created
8790
"""
8891

8992
@abstractmethod
@@ -94,6 +97,9 @@ def delete(self, id_: str, old_props: dict[str, Any]) -> None:
9497
Args:
9598
id_: the ID of the resource
9699
old_props: the outputs from the last create operation
100+
101+
Raises:
102+
An appropriate DataSafeHavenError if the resource could not be deleted
97103
"""
98104

99105
@abstractmethod

data_safe_haven/infrastructure/components/dynamic/ssl_certificate.py

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import time
44
from contextlib import suppress
5-
from typing import Any
5+
from datetime import UTC, datetime, timedelta
6+
from typing import Any, override
67

78
from acme.errors import ValidationError
89
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
@@ -15,6 +16,7 @@
1516
from pulumi import Input, Output, ResourceOptions
1617
from pulumi.dynamic import CreateResult, DiffResult, Resource
1718
from simple_acme_dns import ACMEClient
19+
from simple_acme_dns.errors import InvalidKeyType
1820

1921
from data_safe_haven.exceptions import DataSafeHavenAzureError, DataSafeHavenSSLError
2022
from data_safe_haven.external import AzureSdk
@@ -43,8 +45,8 @@ def __init__(
4345

4446

4547
class SSLCertificateProvider(DshResourceProvider):
48+
@override
4649
def create(self, props: dict[str, Any]) -> CreateResult:
47-
"""Create new SSL certificate."""
4850
outs = dict(**props)
4951
try:
5052
client = ACMEClient(
@@ -60,8 +62,8 @@ def create(self, props: dict[str, Any]) -> CreateResult:
6062
private_key_bytes = client.generate_private_key(key_type="rsa2048")
6163
client.generate_csr()
6264
# Request DNS verification tokens and add them to the DNS record
63-
azure_sdk = AzureSdk(props["subscription_name"], disable_logging=True)
6465
verification_tokens = client.request_verification_tokens().items()
66+
azure_sdk = AzureSdk(props["subscription_name"], disable_logging=True)
6567
for record_name, record_values in verification_tokens:
6668
record_set = azure_sdk.ensure_dns_txt_record(
6769
record_name=record_name.replace(f".{props['domain_name']}", ""),
@@ -96,7 +98,7 @@ def create(self, props: dict[str, Any]) -> CreateResult:
9698
private_key = load_pem_private_key(private_key_bytes, None)
9799
if not isinstance(private_key, RSAPrivateKey):
98100
msg = f"Private key is of type {type(private_key)} not RSAPrivateKey."
99-
raise TypeError(msg)
101+
raise DataSafeHavenSSLError(msg)
100102
all_certs = [
101103
load_pem_x509_certificate(data)
102104
for data in certificate_bytes.split(b"\n\n")
@@ -118,8 +120,16 @@ def create(self, props: dict[str, Any]) -> CreateResult:
118120
certificate_contents=pfx_bytes,
119121
key_vault_name=props["key_vault_name"],
120122
)
121-
outs["secret_id"] = kvcert.secret_id
122-
except Exception as exc:
123+
# Failures here will raise an exception that will be caught below
124+
outs["expiry_date"] = kvcert.properties.expires_on.isoformat()
125+
outs["secret_id"] = "/".join(kvcert.secret_id.split("/")[:-1])
126+
except (
127+
AttributeError,
128+
DataSafeHavenAzureError,
129+
IndexError,
130+
InvalidKeyType,
131+
StopIteration,
132+
) as exc:
123133
cert_name = f"[green]{props['certificate_secret_name']}[/]"
124134
domain_name = f"[green]{props['domain_name']}[/]"
125135
msg = f"Failed to create SSL certificate {cert_name} for {domain_name}."
@@ -129,8 +139,8 @@ def create(self, props: dict[str, Any]) -> CreateResult:
129139
outs=outs,
130140
)
131141

142+
@override
132143
def delete(self, id_: str, props: dict[str, Any]) -> None:
133-
"""Delete an SSL certificate."""
134144
# Use `id` as a no-op to avoid ARG002 while maintaining function signature
135145
id(id_)
136146
try:
@@ -146,43 +156,51 @@ def delete(self, id_: str, props: dict[str, Any]) -> None:
146156
certificate_name=props["certificate_secret_name"],
147157
key_vault_name=props["key_vault_name"],
148158
)
149-
except Exception as exc:
159+
except DataSafeHavenAzureError as exc:
150160
cert_name = f"[green]{props['certificate_secret_name']}[/]"
151161
domain_name = f"[green]{props['domain_name']}[/]"
152162
msg = f"Failed to delete SSL certificate {cert_name} for {domain_name}."
153163
raise DataSafeHavenSSLError(msg) from exc
154164

165+
@override
155166
def diff(
156167
self,
157168
id_: str,
158169
old_props: dict[str, Any],
159170
new_props: dict[str, Any],
160171
) -> DiffResult:
161-
"""Calculate diff between old and new state"""
162172
# Use `id` as a no-op to avoid ARG002 while maintaining function signature
163173
id(id_)
164-
return self.partial_diff(old_props, new_props, [])
174+
partial = self.partial_diff(old_props, new_props, [])
175+
expiry_date = datetime.fromisoformat(
176+
old_props.get("expiry_date", "0001-01-01T00:00:00+00:00")
177+
)
178+
needs_renewal = datetime.now(UTC) + timedelta(days=30) > expiry_date
179+
return DiffResult(
180+
changes=partial.changes or needs_renewal,
181+
replaces=partial.replaces,
182+
stables=partial.stables,
183+
delete_before_replace=True,
184+
)
165185

186+
@override
166187
def refresh(self, props: dict[str, Any]) -> dict[str, Any]:
167-
try:
168-
outs = dict(**props)
169-
with suppress(DataSafeHavenAzureError, KeyError):
170-
azure_sdk = AzureSdk(outs["subscription_name"], disable_logging=True)
171-
certificate = azure_sdk.get_keyvault_certificate(
172-
outs["certificate_secret_name"], outs["key_vault_name"]
173-
)
174-
if certificate.secret_id:
175-
outs["secret_id"] = certificate.secret_id
176-
return outs
177-
except Exception as exc:
178-
cert_name = f"[green]{props['certificate_secret_name']}[/]"
179-
domain_name = f"[green]{props['domain_name']}[/]"
180-
msg = f"Failed to refresh SSL certificate {cert_name} for {domain_name}."
181-
raise DataSafeHavenSSLError(msg) from exc
188+
outs = dict(**props)
189+
with suppress(DataSafeHavenAzureError, KeyError):
190+
azure_sdk = AzureSdk(outs["subscription_name"], disable_logging=True)
191+
kvcert = azure_sdk.get_keyvault_certificate(
192+
outs["certificate_secret_name"], outs["key_vault_name"]
193+
)
194+
if kvcert.secret_id:
195+
outs["secret_id"] = kvcert.secret_id
196+
if kvcert.properties and kvcert.properties.expires_on:
197+
outs["expiry_date"] = kvcert.properties.expires_on.isoformat()
198+
return outs
182199

183200

184201
class SSLCertificate(Resource):
185202
_resource_type_name = "dsh:common:SSLCertificate" # set resource type
203+
expiry_date: Output[str]
186204
secret_id: Output[str]
187205

188206
def __init__(
@@ -194,6 +212,6 @@ def __init__(
194212
super().__init__(
195213
SSLCertificateProvider(),
196214
name,
197-
{"secret_id": None, **vars(props)},
215+
{"expiry_date": None, "secret_id": None, **vars(props)},
198216
opts,
199217
)

data_safe_haven/infrastructure/programs/sre/software_repositories.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def __init__(
187187
],
188188
),
189189
containerinstance.ContainerArgs(
190-
image="sonatype/nexus3:3.71.0",
190+
image="sonatype/nexus3:3.76.0",
191191
name="nexus"[:63],
192192
environment_variables=[],
193193
ports=[],

data_safe_haven/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "5.3.0"
1+
__version__ = "5.3.1"
22
__version_info__ = tuple(__version__.split("."))

docs/source/deployment/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ See [the instructions here](https://docs.docker.com/security/for-developers/acce
4646
$ pipx install data-safe-haven
4747
:::
4848

49-
- Or install a specific version with
49+
- Or install a specific version with (for instance)
5050

5151
:::{code} shell
52-
$ pipx install data-safe-haven==5.0.0
52+
$ pipx install data-safe-haven==5.3.1
5353
:::
5454

5555
::::{admonition} [Advanced] install into a virtual environment

0 commit comments

Comments
 (0)