Skip to content

Commit 55b6af3

Browse files
Merge pull request #184 from NHSDigital/adwa1-PDM-892-pdm-clients
PDM-892: adding HealthLake, STS and ECS client creation functions
2 parents 93227e0 + fb4950e commit 55b6af3

File tree

10 files changed

+443
-284
lines changed

10 files changed

+443
-284
lines changed

.gitallowed

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

22
token: \$\{\{ secrets.GITHUB_TOKEN \}\}
3-
"token": response.get\("SessionToken"\)
3+
"token": response\["SessionToken"\]
44
token=credentials\["token"\]
5-
5+
:123456789012:
6+
\"123456789012\"
7+
pytokens
68
.*(GITHUB|SONAR)_TOKEN: \$\{\{ secrets.(GITHUB|SONAR)_TOKEN \}\}
79
.*asttokens = ">=2.1.0"

.github/workflows/merge-develop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858

5959
- name: setup java
6060
if: github.actor != 'dependabot[bot]' && (success() || failure())
61-
uses: actions/setup-java@v4
61+
uses: actions/setup-java@v5
6262
with:
6363
distribution: "corretto"
6464
java-version: "17"

.github/workflows/pull-request.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
tox:
1212
strategy:
1313
matrix:
14-
python-version: ["3.9", "3.10", "3.11"]
14+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
1515

1616
runs-on: ubuntu-latest
1717
if: github.repository == 'NHSDigital/nhs-aws-helpers'
@@ -145,7 +145,7 @@ jobs:
145145

146146
- name: setup java
147147
if: success() || failure()
148-
uses: actions/setup-java@v4
148+
uses: actions/setup-java@v5
149149
with:
150150
distribution: "corretto"
151151
java-version: "17"

nhs_aws_helpers/__init__.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@
5050
from mypy_boto3_dynamodb.client import DynamoDBClient
5151
from mypy_boto3_dynamodb.service_resource import DynamoDBServiceResource, Table
5252
from mypy_boto3_dynamodb.type_defs import KeysAndAttributesTypeDef
53+
from mypy_boto3_ecs.client import ECSClient
5354
from mypy_boto3_events import EventBridgeClient
5455
from mypy_boto3_firehose import FirehoseClient
5556
from mypy_boto3_glue import GlueClient
57+
from mypy_boto3_healthlake.client import HealthLakeClient
58+
from mypy_boto3_iam import IAMClient
5659
from mypy_boto3_kms.client import KMSClient
5760
from mypy_boto3_lambda.client import LambdaClient
5861
from mypy_boto3_logs.client import CloudWatchLogsClient
@@ -72,6 +75,8 @@
7275
from mypy_boto3_sqs.client import SQSClient
7376
from mypy_boto3_ssm.client import SSMClient
7477
from mypy_boto3_stepfunctions import SFNClient
78+
from mypy_boto3_sts.client import STSClient
79+
from mypy_boto3_sts.type_defs import AssumeRoleRequestTypeDef
7580

7681
from nhs_aws_helpers.common import run_in_executor
7782
from nhs_aws_helpers.s3_object_writer import S3ObjectWriter
@@ -191,6 +196,10 @@ def backup_client(session: Optional[Session] = None, config: Optional[Config] =
191196
return _aws("backup", "client", session, config) # type: ignore[no-any-return]
192197

193198

199+
def iam_client(session: Optional[Session] = None, config: Optional[Config] = None) -> IAMClient:
200+
return _aws("iam", "client", session, config) # type: ignore[no-any-return]
201+
202+
194203
def register_retry_handler(
195204
client_or_resource: Union[S3ServiceResource, S3Client],
196205
on_error: Optional[Callable] = None,
@@ -349,6 +358,18 @@ def events_client(session: Optional[Session] = None, config: Optional[Config] =
349358
return _aws("events", "client", session, config) # type: ignore[no-any-return]
350359

351360

361+
def healthlake_client(session: Optional[Session] = None, config: Optional[Config] = None) -> HealthLakeClient:
362+
return _aws("healthlake", "client", session, config) # type: ignore[no-any-return]
363+
364+
365+
def sts_client(session: Optional[Session] = None, config: Optional[Config] = None) -> STSClient:
366+
return _aws("sts", "client", session, config) # type: ignore[no-any-return]
367+
368+
369+
def ecs_client(session: Optional[Session] = None, config: Optional[Config] = None) -> ECSClient:
370+
return _aws("ecs", "client", session, config) # type: ignore[no-any-return]
371+
372+
352373
def s3_bucket(bucket: str, session: Optional[Session] = None, config: Optional[Config] = None) -> Bucket:
353374
return s3_resource(session=session, config=config).Bucket(bucket)
354375

@@ -1041,19 +1062,19 @@ def assumed_credentials(
10411062

10421063
sts_client = boto3.client("sts", region_name=region, endpoint_url=endpoint_url)
10431064

1044-
params = {
1045-
"RoleArn": f"arn:aws:iam::{account_id}:role/{role}",
1046-
"RoleSessionName": role_session_name,
1047-
"DurationSeconds": duration_seconds,
1048-
}
1065+
params = AssumeRoleRequestTypeDef(
1066+
RoleArn=f"arn:aws:iam::{account_id}:role/{role}",
1067+
RoleSessionName=role_session_name,
1068+
DurationSeconds=duration_seconds,
1069+
)
10491070

1050-
response = sts_client.assume_role(**params).get("Credentials")
1071+
response = sts_client.assume_role(**params)["Credentials"]
10511072

10521073
credentials = {
1053-
"access_key": response.get("AccessKeyId"),
1054-
"secret_key": response.get("SecretAccessKey"),
1055-
"token": response.get("SessionToken"),
1056-
"expiry_time": response.get("Expiration").isoformat(),
1074+
"access_key": response["AccessKeyId"],
1075+
"secret_key": response["SecretAccessKey"],
1076+
"token": response["SessionToken"],
1077+
"expiry_time": response["Expiration"].isoformat(),
10571078
}
10581079
return credentials
10591080

poetry.lock

Lines changed: 338 additions & 261 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ repository = "https://github.com/NHSDigital/nhs-aws-helpers"
1515
# core dependencies
1616
python = ">=3.9,<4.0"
1717
boto3 = "^1.38.14"
18-
boto3-stubs = {extras = ["s3", "ssm", "secretsmanager", "dynamodb", "stepfunctions", "sqs", "lambda", "logs", "ses", "sns", "events", "kms", "firehose", "athena", "glue", "ce", "cloudwatch", "backup"], version = "^1.38.6"}
18+
boto3-stubs = {extras = ["s3", "ssm", "secretsmanager", "dynamodb", "stepfunctions", "sqs", "lambda", "logs", "ses", "sns", "events", "kms", "firehose", "athena", "glue", "ce", "cloudwatch", "backup", "healthlake", "sts", "ecs", "iam"], version = "^1.38.6"}
1919
botocore-stubs = "^1.38.46"
2020

2121

scripts/hooks/commit-msg.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ message_file="${1}"
66

77
commit_message="$(tr '[:upper:]' '[:lower:]' < "${message_file}")"
88

9-
if [[ "${commit_message}" =~ ^(((mesh|spinecore)?\-[0-9]+\:?\ )|(merge\ branch)).* ]]; then
9+
if [[ "${commit_message}" =~ ^(((mesh|spinecore|pdm)?\-[0-9]+\:?\ )|(merge\ branch)).* ]]; then
1010
exit 0
1111
else
1212
echo ""

tests/aws_tests.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
import json
12
import logging
23
import os
3-
from typing import Any, cast
4+
from typing import Any, Literal, TypedDict, cast
45
from uuid import uuid4
56

67
import pytest
@@ -10,7 +11,9 @@
1011
from pytest_httpserver import HTTPServer
1112

1213
from nhs_aws_helpers import (
14+
assumed_credentials,
1315
dynamodb_retry_backoff,
16+
iam_client,
1417
post_create_client,
1518
register_config_default,
1619
register_retry_handler,
@@ -247,14 +250,20 @@ def test_s3_retries(
247250
key = f"{expected_folder}/filename.txt"
248251
httpserver.expect_request(f"/{temp_s3_bucket.name}/{key}").respond_with_json({}, status=503)
249252

253+
# Type-identical spec to match botocore's internal typing of _RetryDict
254+
class BotocoreRetries(TypedDict, total=False):
255+
total_max_attempts: int
256+
max_attempts: int
257+
mode: Literal["legacy", "standard", "adaptive"]
258+
250259
config = Config(
251260
connect_timeout=float(os.environ.get("BOTO_CONNECT_TIMEOUT", "1")),
252261
read_timeout=float(os.environ.get("BOTO_READ_TIMEOUT", "1")),
253262
max_pool_connections=int(os.environ.get("BOTO_MAX_POOL_CONNECTIONS", "10")),
254-
retries={
255-
"mode": os.environ.get("BOTO_RETRIES_MODE", "standard"), # type: ignore[typeddict-item]
256-
"total_max_attempts": int(os.environ.get("BOTO_RETRIES_TOTAL_MAX_ATTEMPTS", "2")),
257-
},
263+
retries=BotocoreRetries(
264+
mode=os.environ.get("BOTO_RETRIES_MODE", "standard"), # type: ignore[typeddict-item]
265+
total_max_attempts=int(os.environ.get("BOTO_RETRIES_TOTAL_MAX_ATTEMPTS", "2")),
266+
),
258267
)
259268

260269
def _post_create_aws(boto_module: str, _: str, client):
@@ -359,3 +368,28 @@ def test_s3_upload_multipart_from_copy_missing_part_data(temp_s3_bucket: Bucket)
359368
target_object.get()
360369

361370
assert client_error.value.response["Error"]["Code"] == "NoSuchKey"
371+
372+
373+
def test_assumed_credentials():
374+
iam = iam_client()
375+
user_name = uuid4().hex
376+
role_name = uuid4().hex
377+
378+
iam.create_user(UserName=user_name)
379+
380+
role_arn = f"arn:aws:iam::123456789012:role/{role_name}"
381+
382+
policy_document = {
383+
"Version": "2012-10-17",
384+
"Statement": [{"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": role_arn}],
385+
}
386+
387+
iam.put_user_policy(UserName=user_name, PolicyName="AllowAssumeRole", PolicyDocument=json.dumps(policy_document))
388+
389+
creds = assumed_credentials(
390+
account_id="123456789012", role=role_name, sts_endpoint_url=os.environ["AWS_ENDPOINT_URL"]
391+
)
392+
assert creds["access_key"]
393+
assert creds["secret_key"]
394+
assert creds["token"]
395+
assert creds["expiry_time"]

tests/client_tests.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from nhs_aws_helpers import ecs_client, healthlake_client, iam_client, sts_client
2+
3+
4+
def test_sts_client():
5+
client = sts_client()
6+
assert client.meta.service_model.service_id == "STS"
7+
8+
9+
def test_healthlake_client():
10+
client = healthlake_client()
11+
assert client.meta.service_model.service_id == "HealthLake"
12+
13+
14+
def test_ecs_client():
15+
client = ecs_client()
16+
assert client.meta.service_model.service_id == "ECS"
17+
18+
19+
def test_iam_client():
20+
client = iam_client()
21+
assert client.meta.service_model.service_id == "IAM"

tests/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,9 @@
77

88
@pytest.fixture(scope="session", autouse=True)
99
def autouse():
10-
with temp_env_vars(AWS_ENDPOINT_URL="http://localhost:4566"):
10+
with temp_env_vars(
11+
AWS_ENDPOINT_URL="http://localhost:4566",
12+
AWS_ACCESS_KEY_ID="dummy_access_key",
13+
AWS_SECRET_ACCESS_KEY="dummy_secret_key",
14+
):
1115
yield

0 commit comments

Comments
 (0)