Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6aa445d
Add pyjwt, clinical_jwt, make ruff happy
Vox-Ben Feb 20, 2026
0193f8d
Review comments
Vox-Ben Feb 26, 2026
00721cc
Make ruff happy, refactor controller JWT test
Vox-Ben Mar 2, 2026
ed4a788
Start of orange box connection
Vox-Ben Mar 4, 2026
f60a679
Orange box working with debug script
Vox-Ben Mar 5, 2026
b3da866
Orange box connection working
Vox-Ben Mar 6, 2026
95c0ec1
Pass $STUB_PROVIDER
Vox-Ben Mar 6, 2026
932fec3
Pass STUB env vars
Vox-Ben Mar 6, 2026
1515f7f
Create call script
Vox-Ben Mar 6, 2026
1ebab25
Fix algorithm string none
Vox-Ben Mar 9, 2026
f2bf4ec
Tidy up error handling
Vox-Ben Mar 10, 2026
9470f2e
Add request validation to provider stub
Vox-Ben Mar 11, 2026
9d8fd47
Fix rebase mess and tidy up
Vox-Ben Mar 12, 2026
3c196a0
Update TODO comment
Vox-Ben Mar 12, 2026
373db76
Improve test coverage
Vox-Ben Mar 12, 2026
49467f4
Fix JWT default times and encoding value
Vox-Ben Mar 12, 2026
a6cc0cf
Move FHIR constants to single enum
Vox-Ben Mar 12, 2026
6c58250
Remove now-duplicate scripts dir
Vox-Ben Mar 12, 2026
96503c9
Change PROVIDER_STRING variable name
Vox-Ben Mar 16, 2026
36c7a1a
Why is Ruff complaining about this now and not six weeks ago?
Vox-Ben Mar 16, 2026
d181334
Review comments
Vox-Ben Mar 16, 2026
c56c3d1
Put Any type back in
Vox-Ben Mar 16, 2026
c9fbb77
Fix Trivy error
Vox-Ben Mar 17, 2026
c28f8fb
Copilot review None check
Vox-Ben Mar 17, 2026
e8ccff7
Add certs, remove run script
Vox-Ben Mar 17, 2026
25c0cec
Copilot review fixes
Vox-Ben Mar 20, 2026
e3d48b1
Remove cert file check again
Vox-Ben Mar 20, 2026
ca31e79
Add space in content-type header
Vox-Ben Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/preview-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -507,20 +507,20 @@ jobs:
# ---------- Security scanning ----------
- name: Trivy IaC scan
if: github.event.action != 'closed'
uses: nhs-england-tools/trivy-action/iac-scan@3456c1657a37d500027fd782e6b08911725392da
uses: nhs-england-tools/trivy-action/iac-scan@289984b2f03034233a347d6dbadecd5ca9ea9634
with:
scan-ref: infrastructure/environments/preview
artifact-name: trivy-iac-scan-${{ steps.meta.outputs.branch_name }}

- name: Trivy filesystem scan
if: github.event.action != 'closed'
uses: nhs-england-tools/trivy-action/image-scan@3456c1657a37d500027fd782e6b08911725392da
uses: nhs-england-tools/trivy-action/image-scan@289984b2f03034233a347d6dbadecd5ca9ea9634
with:
image-ref: ${{steps.meta.outputs.ecr_url}}:${{steps.meta.outputs.branch_name}}
artifact-name: trivy-scan-${{ steps.meta.outputs.branch_name }}

- name: Generate SBOM
uses: nhs-england-tools/trivy-action/sbom-scan@3456c1657a37d500027fd782e6b08911725392da
uses: nhs-england-tools/trivy-action/sbom-scan@289984b2f03034233a347d6dbadecd5ca9ea9634
if: github.event.action != 'closed'
with:
image-ref: ${{steps.meta.outputs.ecr_url}}:${{steps.meta.outputs.branch_name}}
Expand Down
20 changes: 17 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,25 @@ publish: # Publish the project artefact @Pipeline
# TODO: Implement the artefact publishing step

deploy: clean build # Deploy the project artefact to the target environment @Pipeline
@if [[ -n "$${IN_BUILD_CONTAINER}" ]]; then \
# Build up list of environment variables to pass to the container
@ENVIRONMENT_STRING="" ; \
if [[ -n "$${STUB_PROVIDER}" ]]; then \
ENVIRONMENT_STRING="$${ENVIRONMENT_STRING} -e STUB_PROVIDER=$${STUB_PROVIDER}" ; \
fi ; \
if [[ -n "$${STUB_PDS}" ]]; then \
ENVIRONMENT_STRING="$${ENVIRONMENT_STRING} -e STUB_PDS=$${STUB_PDS}" ; \
fi ; \
if [[ -n "$${STUB_SDS}" ]]; then \
ENVIRONMENT_STRING="$${ENVIRONMENT_STRING} -e STUB_SDS=$${STUB_SDS}" ; \
fi ; \
if [[ -n "$${CDG_DEBUG}" ]]; then \
ENVIRONMENT_STRING="$${ENVIRONMENT_STRING} -e CDG_DEBUG=$${CDG_DEBUG}" ; \
fi ; \
if [[ -n "$${IN_BUILD_CONTAINER}" ]]; then \
echo "Starting using local docker network ..." ; \
$(docker) run --platform linux/amd64 --name gateway-api -p 5000:8080 --network gateway-local -d ${IMAGE_NAME} ; \
$(docker) run --platform linux/amd64 --name gateway-api -p 5000:8080 --network gateway-local $${ENVIRONMENT_STRING} -d ${IMAGE_NAME} ; \
else \
$(docker) run --platform linux/amd64 --name gateway-api -p 5000:8080 -d ${IMAGE_NAME} ; \
$(docker) run --platform linux/amd64 --name gateway-api -p 5000:8080 $${ENVIRONMENT_STRING} -d ${IMAGE_NAME} ; \
fi

clean:: stop # Clean-up project resources (main) @Operations
Expand Down
6 changes: 3 additions & 3 deletions gateway-api/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions gateway-api/src/fhir/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from enum import StrEnum


class FHIRSystem(StrEnum):
"""
Enum for FHIR identifier systems used in the clinical data gateway.
"""

NHS_NUMBER = "https://fhir.nhs.uk/Id/nhs-number"
ODS_CODE = "https://fhir.nhs.uk/Id/ods-organization-code"
SDS_USER_ID = "https://fhir.nhs.uk/Id/sds-user-id"
SDS_ROLE_PROFILE_ID = "https://fhir.nhs.uk/Id/sds-role-profile-id"
NHS_SERVICE_INTERACTION_ID = "https://fhir.nhs.uk/Id/nhsServiceInteractionId"
NHS_MHS_PARTY_KEY = "https://fhir.nhs.uk/Id/nhsMhsPartyKey"
NHS_SPINE_ASID = "https://fhir.nhs.uk/Id/nhsSpineASID"
4 changes: 3 additions & 1 deletion gateway-api/src/gateway_api/clinical_jwt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from .device import Device
from .jwt import JWT
from .organization import Organization
from .practitioner import Practitioner
from .validator import JWTValidator

__all__ = ["JWT", "Device", "Practitioner"]
__all__ = ["JWT", "Device", "Organization", "Practitioner", "JWTValidator"]
28 changes: 10 additions & 18 deletions gateway-api/src/gateway_api/clinical_jwt/device.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True, kw_only=True)
Expand All @@ -8,22 +9,13 @@ class Device:
model: str
version: str

@property
def json(self) -> str:
outstr = f"""
{{
"resourceType": "Device",
"identifier": [
{{
"system": "{self.system}",
"value": "{self.value}"
}}
],
"model": "{self.model}",
"version": "{self.version}"
}}
def to_dict(self) -> dict[str, Any]:
"""
return outstr.strip()

def __str__(self) -> str:
return self.json
Return the Device as a dictionary suitable for JWT payload.
"""
return {
"resourceType": "Device",
"identifier": [{"system": self.system, "value": self.value}],
"model": self.model,
"version": self.version,
}
14 changes: 9 additions & 5 deletions gateway-api/src/gateway_api/clinical_jwt/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,24 @@ class JWT:
issuer: str
subject: str
audience: str
requesting_device: str
requesting_organization: str
requesting_practitioner: str
requesting_device: dict[str, Any]
requesting_organization: dict[str, Any]
requesting_practitioner: dict[str, Any]

# Time fields
issued_at: int = field(default_factory=lambda: int(time()))
expiration: int = field(default_factory=lambda: int(time()) + 300)
expiration: int = 0

# These are here for future proofing but are not expected ever to be changed
algorithm: str | None = None
algorithm: str = "none"
type: str = "JWT"
reason_for_request: str = "directcare"
requested_scope: str = "patient/*.read"

def __post_init__(self) -> None:
if self.expiration == 0:
object.__setattr__(self, "expiration", self.issued_at + 300)

@property
def issue_time(self) -> str:
return datetime.fromtimestamp(self.issued_at, tz=UTC).isoformat()
Expand Down
25 changes: 25 additions & 0 deletions gateway-api/src/gateway_api/clinical_jwt/organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from dataclasses import dataclass
from typing import Any

from fhir.constants import FHIRSystem


@dataclass(frozen=True, kw_only=True)
class Organization:
ods_code: str
name: str

def to_dict(self) -> dict[str, Any]:
"""
Return the Organization as a dictionary suitable for JWT payload.
"""
return {
"resourceType": "Organization",
"identifier": [
{
"system": FHIRSystem.ODS_CODE,
"value": self.ods_code,
}
],
"name": self.name,
}
60 changes: 27 additions & 33 deletions gateway-api/src/gateway_api/clinical_jwt/practitioner.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from dataclasses import dataclass
from typing import Any

from fhir.constants import FHIRSystem

@dataclass(kw_only=True)

@dataclass(frozen=True, kw_only=True)
class Practitioner:
id: str
sds_userid: str
Expand All @@ -12,38 +15,29 @@ class Practitioner:
given_name: str | None = None
prefix: str | None = None

def __post_init__(self) -> None:
given = "" if self.given_name is None else f',"given":["{self.given_name}"]'
prefix = "" if self.prefix is None else f',"prefix":["{self.prefix}"]'
self._name_str = f'[{{"family": "{self.family_name}"{given}{prefix}}}]'

@property
def json(self) -> str:
user_id_system = "https://fhir.nhs.uk/Id/sds-user-id"
role_id_system = "https://fhir.nhs.uk/Id/sds-role-profile-id"
def _build_name(self) -> list[dict[str, Any]]:
"""Build the name array with proper structure for JWT."""
name_dict: dict[str, Any] = {"family": self.family_name}
if self.given_name is not None:
name_dict["given"] = [self.given_name]
if self.prefix is not None:
name_dict["prefix"] = [self.prefix]
return [name_dict]

outstr = f"""
{{
"resourceType": "Practitioner",
"id": "{self.id}",
"identifier": [
{{
"system": "{user_id_system}",
"value": "{self.sds_userid}"
}},
{{
"system": "{role_id_system}",
"value": "{self.role_profile_id}"
}},
{{
"system": "{self.userid_url}",
"value": "{self.userid_value}"
}}
],
"name": {self._name_str}
}}
def to_dict(self) -> dict[str, Any]:
"""
Return the Practitioner as a dictionary suitable for JWT payload.
"""
return outstr.strip()
user_id_system = FHIRSystem.SDS_USER_ID
role_id_system = FHIRSystem.SDS_ROLE_PROFILE_ID

def __str__(self) -> str:
return self.json
return {
"resourceType": "Practitioner",
"id": self.id,
"identifier": [
{"system": user_id_system, "value": self.sds_userid},
{"system": role_id_system, "value": self.role_profile_id},
{"system": self.userid_url, "value": self.userid_value},
],
"name": self._build_name(),
}
Empty file.
19 changes: 1 addition & 18 deletions gateway-api/src/gateway_api/clinical_jwt/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
Unit tests for :mod:`gateway_api.clinical_jwt.device`.
"""

from json import loads

from gateway_api.clinical_jwt import Device


Expand Down Expand Up @@ -35,8 +33,7 @@ def test_device_json_property_returns_valid_json_structure() -> None:
version="5.3.0",
)

json_output = input_device.json
jdict = loads(json_output)
jdict = input_device.to_dict()

output_device = Device(
system=jdict["identifier"][0]["system"],
Expand All @@ -46,17 +43,3 @@ def test_device_json_property_returns_valid_json_structure() -> None:
)

assert input_device == output_device


def test_device_str_returns_json() -> None:
"""
Test that __str__ returns the same value as the json property.
"""
device = Device(
system="https://test.com/device",
value="TEST-001",
model="Test Model",
version="1.0.0",
)

assert str(device) == device.json
Loading
Loading