Skip to content

Commit 2055b5f

Browse files
CCM-10922 Adding sample authorizer
1 parent 5363f77 commit 2055b5f

File tree

13 files changed

+214
-7540
lines changed

13 files changed

+214
-7540
lines changed

.tool-versions

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ act 0.2.64
22
gitleaks 8.24.0
33
jq 1.6
44
nodejs 22.11.0
5-
pnpm 10.4.1
65
pre-commit 3.6.0
76
python 3.13.2
87
terraform 1.9.2

infrastructure/terraform/components/api/iam_role_api_gateway_execution_role.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" {
4848
]
4949

5050
resources = [
51-
# module.authorizer_lambda.function_arn,
51+
module.authorizer_lambda.function_arn,
5252
module.hello_world.function_arn,
5353
]
5454
}

infrastructure/terraform/components/api/locals.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ locals {
44
openapi_spec = templatefile("${path.module}/resources/spec.tmpl.json", {
55
APIG_EXECUTION_ROLE_ARN = aws_iam_role.api_gateway_execution_role.arn
66
AWS_REGION = var.region
7-
# AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
7+
AUTHORIZER_LAMBDA_ARN = module.authorizer_lambda.function_arn
88
HELLO_WORLD_LAMBDA_ARN = module.hello_world.function_arn
99
})
1010
}
Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
# module "authorizer_lambda" {
2-
# source = "git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda?ref=v2.0.4"
1+
module "authorizer_lambda" {
2+
source = "git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda?ref=v2.0.4"
33

4-
# aws_account_id = var.aws_account_id
5-
# component = var.component
6-
# environment = var.environment
7-
# project = var.project
8-
# region = var.region
9-
# group = var.group
4+
aws_account_id = var.aws_account_id
5+
component = var.component
6+
environment = var.environment
7+
project = var.project
8+
region = var.region
9+
group = var.group
1010

11-
# log_retention_in_days = var.log_retention_in_days
12-
# kms_key_arn = var.kms_key_arn
11+
log_retention_in_days = var.log_retention_in_days
12+
kms_key_arn = module.kms.key_arn
1313

14-
# function_name = "authorizer"
15-
# description = "Authorizer for Suppliers API"
14+
function_name = "authorizer"
15+
description = "Authorizer for Suppliers API"
1616

17-
# memory = 512
18-
# timeout = 20
19-
# runtime = "nodejs22.x"
17+
memory = 512
18+
timeout = 20
19+
runtime = "nodejs22.x"
2020

21-
# function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
22-
# function_code_base_path = local.aws_lambda_functions_dir_path
23-
# function_code_dir = "authorizer/dist"
24-
# function_module_name = "index"
25-
# handler_function_name = "handler"
21+
function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
22+
function_code_base_path = local.aws_lambda_functions_dir_path
23+
function_code_dir = "authorizer/dist"
24+
function_module_name = "index"
25+
handler_function_name = "handler"
2626

27-
# enable_lambda_insights = false
28-
# force_lambda_code_deploy = var.force_lambda_code_deploy
29-
# }
27+
enable_lambda_insights = false
28+
force_lambda_code_deploy = var.force_lambda_code_deploy
29+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
# Define the outputs for the component. The outputs may well be referenced by other component in the same or different environments using terraform_remote_state data sources...
1+
output "api_urll" {
2+
value = aws_api_gateway_stage.main.invoke_url
3+
}

infrastructure/terraform/components/api/resources/spec.tmpl.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
{
22
"components": {
33
"securitySchemes": {
4+
"LambdaAuthorizer": {
5+
"type": "apiKey",
6+
"name": "Authorization",
7+
"in": "header",
8+
"x-amazon-apigateway-authtype": "custom",
9+
"x-amazon-apigateway-authorizer": {
10+
"type": "request",
11+
"authorizerUri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${AUTHORIZER_LAMBDA_ARN}/invocations",
12+
"authorizerCredentials": "${APIG_EXECUTION_ROLE_ARN}",
13+
"identitySource": "method.request.header.HeaderAuth1",
14+
"authorizerResultTtlInSeconds": 300
15+
}
16+
}
417
}
518
},
619
"info": {
@@ -32,7 +45,12 @@
3245
"timeoutInMillis": 29000,
3346
"type": "AWS_PROXY",
3447
"uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${HELLO_WORLD_LAMBDA_ARN}/invocations"
35-
}
48+
},
49+
"security": [
50+
{
51+
"LambdaAuthorizer": []
52+
}
53+
]
3654
}
3755
}
3856
}

infrastructure/terraform/components/api/variables.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ variable "log_retention_in_days" {
6363
default = 0
6464
}
6565

66+
variable "log_level" {
67+
type = string
68+
description = "The log level to be used in lambda functions within the component. Any log with a lower severity than the configured value will not be logged: https://docs.python.org/3/library/logging.html#levels"
69+
default = "INFO"
70+
}
71+
6672
variable "force_lambda_code_deploy" {
6773
type = bool
6874
description = "If the lambda package in s3 has the same commit id tag as the terraform build branch, the lambda will not update automatically. Set to True if making changes to Lambda code from on the same commit for example during development"

lambdas/authorizer/jest.config.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,60 @@
11
import type { Config } from 'jest';
2-
import { baseJestConfig } from 'nhs-notify-web-template-management-utils';
32

4-
const config: Config = {
3+
export const baseJestConfig: Config = {
4+
preset: 'ts-jest',
5+
6+
// Automatically clear mock calls, instances, contexts and results before every test
7+
clearMocks: true,
8+
9+
// Indicates whether the coverage information should be collected while executing the test
10+
collectCoverage: true,
11+
12+
// The directory where Jest should output its coverage files
13+
coverageDirectory: './.reports/unit/coverage',
14+
15+
// Indicates which provider should be used to instrument code for coverage
16+
coverageProvider: 'babel',
17+
18+
coverageThreshold: {
19+
global: {
20+
branches: 100,
21+
functions: 100,
22+
lines: 100,
23+
statements: -10,
24+
},
25+
},
26+
27+
coveragePathIgnorePatterns: ['/__tests__/'],
28+
transform: { '^.+\\.ts$': 'ts-jest' },
29+
testPathIgnorePatterns: ['.build'],
30+
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
31+
32+
// Use this configuration option to add custom reporters to Jest
33+
reporters: [
34+
'default',
35+
[
36+
'jest-html-reporter',
37+
{
38+
pageTitle: 'Test Report',
39+
outputPath: './.reports/unit/test-report.html',
40+
includeFailureMsg: true,
41+
},
42+
],
43+
],
44+
45+
// The test environment that will be used for testing
46+
testEnvironment: 'jsdom',
47+
};
48+
49+
const utilsJestConfig = {
550
...baseJestConfig,
51+
652
testEnvironment: 'node',
53+
54+
coveragePathIgnorePatterns: [
55+
...(baseJestConfig.coveragePathIgnorePatterns ?? []),
56+
'zod-validators.ts',
57+
],
758
};
859

9-
export default config;
60+
export default utilsJestConfig;
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { handler } from '../index';
2+
import { APIGatewayRequestAuthorizerEvent, Context, Callback } from 'aws-lambda';
3+
4+
describe('Authorizer Lambda Function', () => {
5+
let mockEvent: APIGatewayRequestAuthorizerEvent;
6+
let mockContext: Context;
7+
let mockCallback: jest.MockedFunction<Callback<any>>;
8+
9+
beforeEach(() => {
10+
mockEvent = {
11+
type: 'REQUEST',
12+
methodArn: 'arn:aws:execute-api:region:account-id:api-id/stage/GET/resource',
13+
headers: {},
14+
pathParameters: {}
15+
} as APIGatewayRequestAuthorizerEvent;
16+
17+
mockContext = {} as Context;
18+
mockCallback = jest.fn();
19+
});
20+
21+
it('Should allow access when headers match', () => {
22+
mockEvent.headers = { HeaderAuth1: 'headerValue1' };
23+
24+
handler(mockEvent, mockContext, mockCallback);
25+
26+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
27+
policyDocument: expect.objectContaining({
28+
Statement: expect.arrayContaining([
29+
expect.objectContaining({
30+
Effect: 'Allow',
31+
}),
32+
]),
33+
}),
34+
}));
35+
});
36+
37+
it('Should deny access when headers do not match', () => {
38+
mockEvent.headers = { HeaderAuth1: 'wrongValue' };
39+
40+
handler(mockEvent, mockContext, mockCallback);
41+
42+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
43+
policyDocument: expect.objectContaining({
44+
Statement: expect.arrayContaining([
45+
expect.objectContaining({
46+
Effect: 'Deny',
47+
}),
48+
]),
49+
}),
50+
}));
51+
});
52+
53+
it('Should handle null headers gracefully', () => {
54+
mockEvent.headers = null;
55+
56+
handler(mockEvent, mockContext, mockCallback);
57+
58+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
59+
policyDocument: expect.objectContaining({
60+
Statement: expect.arrayContaining([
61+
expect.objectContaining({
62+
Effect: 'Deny',
63+
}),
64+
]),
65+
}),
66+
}));
67+
});
68+
69+
it('Should handle defined headers correctly', () => {
70+
mockEvent.headers = { HeaderAuth1: 'headerValue1' };
71+
72+
handler(mockEvent, mockContext, mockCallback);
73+
74+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
75+
policyDocument: expect.objectContaining({
76+
Statement: expect.arrayContaining([
77+
expect.objectContaining({
78+
Effect: 'Allow',
79+
}),
80+
]),
81+
}),
82+
}));
83+
});
84+
});

lambdas/authorizer/src/index.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,7 @@ export const handler = (
2020
): void => {
2121
console.log('Received event:', JSON.stringify(event, null, 2));
2222

23-
// Retrieve request parameters from the Lambda function input:
2423
const headers = event.headers || {};
25-
const pathParameters = event.pathParameters || {};
26-
const stageVariables = event.stageVariables || {};
27-
28-
// Parse the input for the parameter values
2924
const tmp = event.methodArn.split(':');
3025
const apiGatewayArnTmp = tmp[5].split('/');
3126
const awsAccountId = tmp[4];
@@ -34,15 +29,15 @@ export const handler = (
3429
const stage = apiGatewayArnTmp[1];
3530
const method = apiGatewayArnTmp[2];
3631
let resource = '/'; // root resource
32+
3733
if (apiGatewayArnTmp[3]) {
3834
resource += apiGatewayArnTmp[3];
3935
}
4036

4137
// Perform authorization to return the Allow policy for correct parameters and
4238
// the 'Unauthorized' error, otherwise.
4339
if (
44-
headers['HeaderAuth1'] === 'headerValue1' &&
45-
stageVariables['StageVar1'] === 'stageValue1'
40+
headers['HeaderAuth1'] === 'headerValue1'
4641
) {
4742
callback(null, generateAllow('me', event.methodArn));
4843
} else {

0 commit comments

Comments
 (0)