Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 41 additions & 0 deletions .github/actions/proxy/configure-proxygen/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Configure Proxygen
description: Install yq for yaml, install proxygen-cli and configure the account

inputs:
proxygen-key-secret:
description: 'Proxygen private key secret'
required: true
proxygen-key-id:
description: 'Proxygen key ID'
required: true
proxygen-api-name:
description: 'Proxygen API name'
required: true
proxygen-client-id:
description: 'Proxygen client ID'
required: true

runs:
using: composite
steps:
- name: Install yq for YAML template processing
uses: mikefarah/yq@2be0094729a1006f61e8339ce9934bfb3cbb549f # v4.52.2

- name: Install Proxygen CLI
shell: bash
run: |
pip install proxygen-cli
proxygen --version

- name: Configure proxygen account details
shell: bash
working-directory: proxygen
run: |
cp settings.template.yaml $HOME/.proxygen/settings.yaml
yq eval '.api = "${{ inputs.proxygen-api-name }}"' -i $HOME/.proxygen/settings.yaml

printf "%s" "${{ inputs.proxygen-key-secret }}" > /tmp/proxygen_private_key.pem
cp credentials.template.yaml $HOME/.proxygen/credentials.yaml
yq eval '.private_key_path = "/tmp/proxygen_private_key.pem"' -i $HOME/.proxygen/credentials.yaml
yq eval '.key_id = "${{ inputs.proxygen-key-id }}"' -i $HOME/.proxygen/credentials.yaml
yq eval '.client_id = "${{ inputs.proxygen-client-id }}"' -i $HOME/.proxygen/credentials.yaml
48 changes: 48 additions & 0 deletions .github/actions/proxy/deploy-proxy/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Deploy API Proxy
description: Deploy the API proxy instance using Proxygen

inputs:
mtls-secret-name:
description: 'mTLS secret name for the proxy'
required: true
target-url:
description: 'Target URL to which the proxy will forward requests'
required: true
proxy-base-path:
description: 'A unique base path for the proxy instance'
required: true
proxygen-key-secret:
description: 'Proxygen private key secret'
required: true
proxygen-key-id:
description: 'Proxygen key ID'
required: true
proxygen-api-name:
description: 'Proxygen API name'
required: true
proxygen-client-id:
description: 'Proxygen client ID'
required: true

runs:
using: composite
steps:
- name: Configure Proxygen
uses: ./.github/actions/proxy/configure-proxygen
with:
proxygen-key-secret: ${{ inputs.proxygen-key-secret }}
proxygen-key-id: ${{ inputs.proxygen-key-id }}
proxygen-api-name: ${{ inputs.proxygen-api-name }}
proxygen-client-id: ${{ inputs.proxygen-client-id }}

- name: Inject secrets into openapi.yaml for deploying proxy
shell: bash
run: |
cat gateway-api/openapi.yaml proxygen/x-nhsd-apim.template.yaml > /tmp/proxy-specification.yaml

yq eval '.x-nhsd-apim.target.url = "${{ inputs.target-url }}" | .x-nhsd-apim.target.security.secret = "${{ inputs.mtls-secret-name }}"' -i /tmp/proxy-specification.yaml

- name: Deploy API proxy
shell: bash
run: |
proxygen instance deploy internal-dev ${{ inputs.proxy-base-path }} /tmp/proxy-specification.yaml --no-confirm
35 changes: 35 additions & 0 deletions .github/actions/proxy/tear-down-proxy/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Tear Down API Proxy
description: Delete the API proxy instance using Proxygen

inputs:
proxy-base-path:
description: 'A unique base path for the proxy instance'
required: true
proxygen-key-secret:
description: 'Proxygen private key secret'
required: true
proxygen-key-id:
description: 'Proxygen key ID'
required: true
proxygen-api-name:
description: 'Proxygen API name'
required: true
proxygen-client-id:
description: 'Proxygen client ID'
required: true

runs:
using: composite
steps:
- name: Configure Proxygen
uses: ./.github/actions/proxy/configure-proxygen
with:
proxygen-key-secret: ${{ inputs.proxygen-key-secret }}
proxygen-key-id: ${{ inputs.proxygen-key-id }}
proxygen-api-name: ${{ inputs.proxygen-api-name }}
proxygen-client-id: ${{ inputs.proxygen-client-id }}

- name: Tear down preview API proxy
shell: bash
run: |
proxygen instance delete internal-dev ${{ inputs.proxy-base-path }} --no-confirm
33 changes: 32 additions & 1 deletion .github/workflows/preview-env.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,35 @@ jobs:
ECS_CLUSTER=$(jq -r '.ecs_cluster_name.value' tf-output.json)
echo "ecs_cluster=$ECS_CLUSTER" >> $GITHUB_OUTPUT

- name: Get proxygen machine user details
id: proxygen-machine-user
uses: aws-actions/aws-secretsmanager-get-secrets@a9a7eb4e2f2871d30dc5b892576fde60a2ecc802
with:
secret-ids: /cds/gateway/dev/proxygen/proxygen-key-secret
name-transformation: lowercase

- name: Deploy preview API proxy
if: github.event.action != 'closed'
uses: ./.github/actions/proxy/deploy-proxy
with:
mtls-secret-name: ${{ vars.PREVIEW_ENV_MTLS_SECRET_NAME}}
target-url: ${{ steps.tf-output.outputs.preview_url }}
proxy-base-path: 'clinical-data-gateway-api-poc-pr-${{ github.event.pull_request.number }}'
proxygen-key-secret: ${{ env._cds_gateway_dev_proxygen_proxygen_key_secret }}
proxygen-key-id: ${{ vars.PREVIEW_ENV_PROXYGEN_KEY_ID }}
proxygen-api-name: ${{ vars.PROXYGEN_API_NAME }}
proxygen-client-id: ${{ vars.PREVIEW_ENV_PROXYGEN_CLIENT_ID }}

- name: Tear down preview API proxy
if: github.event.action == 'closed'
uses: ./.github/actions/proxy/tear-down-proxy
with:
proxy-base-path: 'clinical-data-gateway-api-poc-pr-${{ github.event.pull_request.number }}'
proxygen-key-secret: ${{ env._cds_gateway_dev_proxygen_proxygen_key_secret }}
proxygen-key-id: ${{ vars.PREVIEW_ENV_PROXYGEN_KEY_ID }}
proxygen-api-name: ${{ vars.PROXYGEN_API_NAME }}
proxygen-client-id: ${{ vars.PREVIEW_ENV_PROXYGEN_CLIENT_ID }}

# ---------- Ensure re-deployment (PR updated) ----------
- name: Force ECS service redeployment
if: github.event.action == 'synchronize'
Expand Down Expand Up @@ -263,6 +292,7 @@ jobs:
script: |
const alb = '${{ steps.tf-output.outputs.target_group }}';
const url = '${{ steps.tf-output.outputs.preview_url }}';
const proxy_url = 'https://internal-dev.api.service.nhs.uk/clinical-data-gateway-api-poc-pr-${{ github.event.pull_request.number }}';
const cluster = '${{ steps.tf-output.outputs.ecs_cluster }}';
const service = '${{ steps.tf-output.outputs.ecs_service }}';
const owner = context.repo.owner;
Expand Down Expand Up @@ -303,7 +333,8 @@ jobs:
const lines = [
'**Deployment Complete**',
`- Preview URL: [${url}](${url}) — [Health endpoint](${url}/health)`,
`- Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
` - Smoke Test: ${smokeReadable} (HTTP ${smokeStatus})`,
`- Proxy URL: [${proxy_url}](${proxy_url})`,
`- ECS Cluster: \`${cluster}\``,
`- ECS Service: \`${service}\``,
`- ALB Target: \`${alb}\``,
Expand Down
32 changes: 32 additions & 0 deletions bruno/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Bruno

## `gateway-api` Workspace

### Preview Environment

#### Environment Setup

The collection pulls in secrets from a `.env` file from the top level of the collection, `bruno/gateway-api/preview-env`. To reference these variables within the collection you use `{{process.env.<key>}}`, where `<key>` is the environment variable name in `.env`.

There is a template `.env` file, `bruno/gateway-api/collections/preview-env/.env.template`, to fill in as described below.

##### Test application

The proxy for Gateway API is hosted in Apigee. In order to call an Apigee proxy, a consumer of the API needs an Apigee application. As such, we need an Apigee application through which we can test our API. A static test application has been created for this purpose. You can view its details by going through In order to view its details, go to [the Clinical Data Sharing APIs applications](https://dos-internal.ptl.api.platform.nhs.uk/). when making a call to the API through the proxy, the test applications API key and secret are fed in to the OAuth 2.0 journey as the `CLIENT_KEY` and `CLIENT_SECRET` respectively. As such, you will need a `bruno/gateway-api/preview-preview-env/.env` file containing

```plaintext
CLIENT_ID=<test application's api key>
CLIENT_SECRET=<test application's api secret>
```

Bruno then uses these values when making an auth journey for you.

Given the API is currently set up with CIS2 user-restricted access, and with the above set, when a HTTP request is sent, you will be prompted for username. [Here is a list of available test users](https://digital.nhs.uk/developer/guides-and-documentation/security-and-authorisation/testing-apis-with-our-mock-authorisation-service#test-users-for-cis2-authentication).

##### Proxy instance

The proxy base path defines to which proxy instance your request will be directed. For preview environments, the proxy base path has the GitHub PR number appended to it. As such you will need to add this to your `.env` file so that Bruno can correctly build the URL.

```plaintext
PR_NNUMBER=<pr number from GitHub>
```
5 changes: 5 additions & 0 deletions bruno/gateway-api/collections/preview-env/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# See README.md
PR_NUMBER=<pr number>

CLIENT_ID=<Application's API key>
CLIENT_SECRET=<Application's API secret>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
meta {
name: Access Record Structured
type: http
seq: 1
}

post {
url: https://{{apigee_env}}.api.service.nhs.uk/{{proxy_base_path}}/patient/$gpc.getstructuredrecord
body: json
auth: inherit
}

body:json {
{
"resourceType": "Parameters",
"parameter": [
{
"name": "patientNHSNumber",
"valueIdentifier": {
"system": "https://fhir.nhs.uk/Id/nhs-number",
"value": "9999999999"
}
}
]
}
}

settings {
encodeUrl: true
timeout: 0
}
9 changes: 9 additions & 0 deletions bruno/gateway-api/collections/preview-env/bruno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"version": "1",
"name": "preview-env",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}
32 changes: 32 additions & 0 deletions bruno/gateway-api/collections/preview-env/collection.bru
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
headers {
Ssp-TraceID: test-teace-id
ODS-from: test-ods-code
}

auth {
mode: oauth2
}

auth:oauth2 {
grant_type: authorization_code
callback_url: https://www.example.com/callback
authorization_url: https://internal-dev.api.service.nhs.uk/oauth2-mock/authorize
access_token_url: https://internal-dev.api.service.nhs.uk/oauth2-mock/token
refresh_token_url:
client_id: {{process.env.CLIENT_ID}}
client_secret: {{process.env.CLIENT_SECRET}}
scope:
state: {{$guid}}
pkce: false
credentials_placement: body
credentials_id: Mock Auth Token
token_placement: header
token_header_prefix: Bearer
auto_fetch_token: true
auto_refresh_token: false
}

vars:pre-request {
apigee_env: internal-dev
proxy_base_path: clinical-data-gateway-api-poc-pr-{{process.env.PR_NUMBER}}
}
14 changes: 14 additions & 0 deletions bruno/gateway-api/workspace.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
opencollection: 1.0.0
info:
name: "gateway"
type: workspace

collections:
- name: "steel_thread"
path: "collections\\steel_thread"
- name: "steel_thread"
path: "collections\\preview-env"

specs:

docs: ''
25 changes: 6 additions & 19 deletions gateway-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ info:
servers:
- url: http://localhost:5000
description: Local development server
components:
securitySchemes:
nhs-cis2-aal3:
$ref: https://proxygen.ptl.api.platform.nhs.uk/components/securitySchemes/nhs-cis2-aal3
paths:
/patient/$gpc.getstructuredrecord:
post:
summary: Get structured record
description: Returns a FHIR Bundle containing patient structured record
security:
- nhs-cis2-aal3: []
operationId: getStructuredRecord
parameters:
- in: header
Expand Down Expand Up @@ -241,22 +247,3 @@ paths:
diagnostics:
type: string
example: "Internal server error"
/health:
get:
summary: Health check
description: Returns the health status of the API
operationId: healthCheck
responses:
'200':
description: Service is healthy
content:
application/json:
schema:
type: object
properties:
status:
type: string
example: "healthy"
required:
- status

33 changes: 33 additions & 0 deletions proxygen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Proxygen

Proxygen is the tool created by the API Platform team to support the deployment of NHS APIs.

We use this tool in the pipelines (and manually) to create, destroy and interact more generally with the proxy instances.

For more information on Proxygen, [read the docs](https://nhsd-confluence.digital.nhs.uk/spaces/APM/pages/375329782/Proxygen).

Proxygen needs:

* a settings file stating which API we are attempting to update;
* a credentials file to authenticate us as the owner/maintainer of the API;
* a specification file that outlines the behaviour of the proxy.

## Settings File

This is stored at `proxygen/settings.yaml` and is read by the Proxygen command line tool when it has been requested to make updates to an API.

## Credentials File

A template is stored at `proxygen/credentials.template.yaml` where the `<proxygen_secret_path>` needs to be inserted. This is a path to a file that holds the secret that identifies us as the owner/maintainer of the API.

During the GitHub workflows, the secret is pulled from AWS secrets manager, written to a file and the path to that file is inserted in to `credentials.template.yaml`.

## Specification file

Proxygen deploys an instance of a proxy using a specification file. This is of the OpenAPI format with a custom extension, `x-nhsd-apim` which provide Proxygen with information as to how the proxy should behave. This includes:

* the target endpoint, to which it will forward traffic;
* the scopes that a user needs in order to access the proxy's endpoint;
* a key that the points to the mTLS certificate which the targeted backend expects to be used.

A template, `proxygen/x-nhsd-api.tempalte.yaml`, is concatenated with the general OpenAPI specification for the API, `gateway-api/openapi.yaml`, and the key to the mTLS certificate to be used for that proxy is written in. All of which is then written to a file and the path to that file is passed to Proxygen to deploy the proxy in the stated environment.
7 changes: 7 additions & 0 deletions proxygen/credentials.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
base_url: https://identity.prod.api.platform.nhs.uk/realms/api-producers
client_id: <proxygen machine user client id>
client_secret: ''
key_id: <proxygen_key_id>
password: ''
private_key_path: <proxygen_private_key_path>
username: ''
3 changes: 3 additions & 0 deletions proxygen/settings.template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
api: <proxygen_api_name>
endpoint_url: https://proxygen.prod.api.platform.nhs.uk
spec_output_format: yaml
Loading