Skip to content

Commit 0121de0

Browse files
committed
Further development
1 parent 12be555 commit 0121de0

File tree

12 files changed

+241
-89
lines changed

12 files changed

+241
-89
lines changed

infrastructure/terraform/components/api/module_authorizer_lambda.tf

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ module "authorizer_lambda" {
1111
log_retention_in_days = var.log_retention_in_days
1212
kms_key_arn = module.kms.key_arn
1313

14+
iam_policy_document = {
15+
body = data.aws_iam_policy_document.authorizer_lambda.json
16+
}
17+
1418
function_name = "authorizer"
1519
description = "Authorizer for Suppliers API"
1620

@@ -52,4 +56,18 @@ data "aws_iam_policy_document" "authorizer_lambda" {
5256
"*"
5357
]
5458
}
59+
60+
statement {
61+
sid = "AllowDynamoDBAccess"
62+
effect = "Allow"
63+
64+
actions = [
65+
"dynamodb:Query"
66+
]
67+
68+
resources = [
69+
aws_dynamodb_table.suppliers.arn,
70+
"${aws_dynamodb_table.suppliers.arn}/index/supplier-apim-index"
71+
]
72+
}
5573
}

lambdas/api-handler/src/handlers/__tests__/utils/test-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function makeApiGwEvent(
1818
requestContext: {
1919
accountId: '123456789012',
2020
apiId: 'api-id',
21-
authorizer: {},
21+
authorizer: null,
2222
protocol: 'HTTP/1.1',
2323
httpMethod: 'GET',
2424
identity: {} as any,
Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { APIGatewayProxyHandler } from "aws-lambda";
2-
import { assertNotEmpty, validateCommonHeaders } from "../utils/validation";
2+
import { assertNotEmpty } from "../utils/validation";
3+
import { extractCommonIds } from '../utils/commonIds';
34
import { ApiErrorDetail } from '../contracts/errors';
45
import { mapErrorToResponse } from "../mappers/error-mapper";
56
import { ValidationError } from "../errors";
@@ -11,10 +12,10 @@ export function createGetLetterDataHandler(deps: Deps): APIGatewayProxyHandler {
1112

1213
return async (event) => {
1314

14-
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
15+
const commonIds = extractCommonIds(event.headers, event.requestContext, deps);
1516

16-
if (!commonHeadersResult.ok) {
17-
return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
17+
if (!commonIds.ok) {
18+
return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger);
1819
}
1920

2021
try {
@@ -24,13 +25,13 @@ export function createGetLetterDataHandler(deps: Deps): APIGatewayProxyHandler {
2425
return {
2526
statusCode: 303,
2627
headers: {
27-
'Location': await getLetterDataUrl(commonHeadersResult.value.supplierId, letterId, deps)
28+
'Location': await getLetterDataUrl(commonIds.value.supplierId, letterId, deps)
2829
},
2930
body: ''
3031
};
3132
}
3233
catch (error) {
33-
return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
34+
return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger);
3435
}
3536
}
3637
};
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { APIGatewayProxyHandler } from "aws-lambda";
2-
import { assertNotEmpty, validateCommonHeaders } from "../utils/validation";
2+
import { assertNotEmpty } from "../utils/validation";
3+
import { extractCommonIds } from '../utils/commonIds';
34
import { ValidationError } from "../errors";
45
import { ApiErrorDetail } from "../contracts/errors";
56
import { getLetterById } from "../services/letter-operations";
@@ -12,22 +13,22 @@ export function createGetLetterHandler(deps: Deps): APIGatewayProxyHandler {
1213

1314
return async (event) => {
1415

15-
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
16+
const commonIds = extractCommonIds(event.headers, event.requestContext, deps);
1617

17-
if (!commonHeadersResult.ok) {
18-
return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
18+
if (!commonIds.ok) {
19+
return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger);
1920
}
2021

2122
try {
2223
const letterId = assertNotEmpty(event.pathParameters?.id, new ValidationError(ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter));
2324

24-
const letter = await getLetterById(commonHeadersResult.value.supplierId, letterId, deps.letterRepo);
25+
const letter = await getLetterById(commonIds.value.supplierId, letterId, deps.letterRepo);
2526

2627
const response = mapToGetLetterResponse(letter);
2728

2829
deps.logger.info({
2930
description: 'Letter successfully fetched by id',
30-
supplierId: commonHeadersResult.value.supplierId,
31+
supplierId: commonIds.value.supplierId,
3132
letterId
3233
});
3334

@@ -37,7 +38,7 @@ export function createGetLetterHandler(deps: Deps): APIGatewayProxyHandler {
3738
};
3839
} catch (error)
3940
{
40-
return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
41+
return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger);
4142
}
4243
}
4344
}

lambdas/api-handler/src/handlers/get-letters.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { APIGatewayProxyEventQueryStringParameters, APIGatewayProxyHandler } from 'aws-lambda';
22
import { getLettersForSupplier } from '../services/letter-operations';
3-
import { validateCommonHeaders } from '../utils/validation';
3+
import { extractCommonIds } from '../utils/commonIds';
44
import { ApiErrorDetail } from '../contracts/errors';
55
import { mapErrorToResponse } from '../mappers/error-mapper';
66
import { ValidationError } from '../errors';
@@ -16,10 +16,10 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler {
1616

1717
return async (event) => {
1818

19-
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
19+
const commonIds = extractCommonIds(event.headers, event.requestContext, deps);
2020

21-
if (!commonHeadersResult.ok) {
22-
return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
21+
if (!commonIds.ok) {
22+
return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger);
2323
}
2424

2525
try {
@@ -28,7 +28,7 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler {
2828
const limitNumber = getLimitOrDefault(event.queryStringParameters, maxLimit, deps.logger);
2929

3030
const letters = await getLettersForSupplier(
31-
commonHeadersResult.value.supplierId,
31+
commonIds.value.supplierId,
3232
status,
3333
limitNumber,
3434
deps.letterRepo,
@@ -38,7 +38,7 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler {
3838

3939
deps.logger.info({
4040
description: 'Pending letters successfully fetched',
41-
supplierId: commonHeadersResult.value.supplierId,
41+
supplierId: commonIds.value.supplierId,
4242
limitNumber,
4343
status,
4444
lettersCount: letters.length
@@ -50,7 +50,7 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler {
5050
};
5151
}
5252
catch (error) {
53-
return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
53+
return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger);
5454
}
5555
}
5656
};

lambdas/api-handler/src/handlers/patch-letter.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { PatchLetterRequest, PatchLetterRequestSchema } from '../contracts/lette
44
import { ApiErrorDetail } from '../contracts/errors';
55
import { ValidationError } from '../errors';
66
import { mapErrorToResponse } from '../mappers/error-mapper';
7-
import { assertNotEmpty, validateCommonHeaders } from '../utils/validation';
7+
import { assertNotEmpty } from '../utils/validation';
8+
import { extractCommonIds } from '../utils/commonIds';
89
import { mapToLetterDto } from '../mappers/letter-mapper';
910
import type { Deps } from "../config/deps";
1011

@@ -13,10 +14,10 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler {
1314

1415
return async (event) => {
1516

16-
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
17+
const commonIds = extractCommonIds(event.headers, event.requestContext, deps);
1718

18-
if (!commonHeadersResult.ok) {
19-
return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
19+
if (!commonIds.ok) {
20+
return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger);
2021
}
2122

2223
try {
@@ -35,15 +36,15 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler {
3536
else throw error;
3637
}
3738

38-
const updatedLetter = await patchLetterStatus(mapToLetterDto(patchLetterRequest, commonHeadersResult.value.supplierId), letterId, deps.letterRepo);
39+
const updatedLetter = await patchLetterStatus(mapToLetterDto(patchLetterRequest, commonIds.value.supplierId), letterId, deps.letterRepo);
3940

4041
return {
4142
statusCode: 200,
4243
body: JSON.stringify(updatedLetter, null, 2)
4344
};
4445

4546
} catch (error) {
46-
return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
47+
return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger);
4748
}
4849
};
4950
};

lambdas/api-handler/src/handlers/post-mi.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { postMI as postMIOperation } from '../services/mi-operations';
33
import { ApiErrorDetail } from "../contracts/errors";
44
import { ValidationError } from "../errors";
55
import { mapErrorToResponse } from "../mappers/error-mapper";
6-
import { assertNotEmpty, validateCommonHeaders, validateIso8601Timestamp } from "../utils/validation";
6+
import { assertNotEmpty, validateIso8601Timestamp } from "../utils/validation";
7+
import { extractCommonIds } from '../utils/commonIds';
78
import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi";
89
import { mapToMI } from "../mappers/mi-mapper";
910
import { Deps } from "../config/deps";
@@ -12,10 +13,10 @@ export function createPostMIHandler(deps: Deps): APIGatewayProxyHandler {
1213

1314
return async (event) => {
1415

15-
const commonHeadersResult = validateCommonHeaders(event.headers, deps);
16+
const commonIds = extractCommonIds(event.headers, event.requestContext, deps);
1617

17-
if (!commonHeadersResult.ok) {
18-
return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger);
18+
if (!commonIds.ok) {
19+
return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger);
1920
}
2021

2122
try {
@@ -33,15 +34,15 @@ export function createPostMIHandler(deps: Deps): APIGatewayProxyHandler {
3334
}
3435
validateIso8601Timestamp(postMIRequest.data.attributes.timestamp);
3536

36-
const result = await postMIOperation(mapToMI(postMIRequest, commonHeadersResult.value.supplierId), deps.miRepo);
37+
const result = await postMIOperation(mapToMI(postMIRequest, commonIds.value.supplierId), deps.miRepo);
3738

3839
return {
3940
statusCode: 201,
4041
body: JSON.stringify(result, null, 2)
4142
};
4243

4344
} catch (error) {
44-
return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger);
45+
return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger);
4546
}
4647
}
4748
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { APIGatewayProxyEvent } from 'aws-lambda';
2+
import { extractCommonIds } from '../commonIds';
3+
4+
const mockDeps = {
5+
env: {
6+
APIM_CORRELATION_HEADER: 'x-correlation-id',
7+
SUPPLIER_ID_HEADER: 'x-supplier-id',
8+
}
9+
} as any;
10+
11+
const mockContext = {} as APIGatewayProxyEvent['requestContext'];
12+
13+
describe('extractCommonIds', () => {
14+
it('returns error if headers are missing', () => {
15+
expect(extractCommonIds({}, mockContext, mockDeps)).toEqual({
16+
ok: false,
17+
error: expect.any(Error)
18+
});
19+
});
20+
21+
it('returns error if correlation id is missing', () => {
22+
const headers = { 'x-supplier-id': 'SUP123', 'x-request-id': 'REQ123' };
23+
expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({
24+
ok: false,
25+
error: expect.any(Error)
26+
});
27+
});
28+
29+
it('returns error if request id is missing', () => {
30+
const headers = { 'x-correlation-id': 'CORR123', 'x-supplier-id': 'SUP123' };
31+
expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({
32+
ok: false,
33+
error: expect.any(Error),
34+
correlationId: 'CORR123'
35+
});
36+
});
37+
38+
it('returns error if supplier id is missing', () => {
39+
const headers = { 'x-correlation-id': 'CORR123', 'x-request-id': 'REQ123' };
40+
expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({
41+
ok: false,
42+
error: expect.any(Error),
43+
correlationId: 'CORR123'
44+
});
45+
});
46+
47+
it('returns ok and ids if all present', () => {
48+
const headers = {
49+
'x-correlation-id': 'CORR123',
50+
'x-request-id': 'REQ123',
51+
'x-supplier-id': 'SUP123'
52+
};
53+
expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({
54+
ok: true,
55+
value: {
56+
correlationId: 'CORR123',
57+
supplierId: 'SUP123'
58+
}
59+
});
60+
});
61+
62+
it('handles mixed case header names', () => {
63+
const headers = {
64+
'X-Correlation-Id': 'CORR123',
65+
'X-Request-Id': 'REQ123',
66+
'X-Supplier-Id': 'SUP123'
67+
};
68+
expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({
69+
ok: true,
70+
value: {
71+
correlationId: 'CORR123',
72+
supplierId: 'SUP123'
73+
}
74+
});
75+
});
76+
77+
it('uses the supplier id from the authorizer if present', () => {
78+
const headers = { 'x-correlation-id': 'CORR123', 'x-supplier-id': 'SUP123', 'x-request-id': 'REQ123' };
79+
const context = { 'authorizer': {'principalId': 'SUP456'}} as unknown as APIGatewayProxyEvent['requestContext'];
80+
expect(extractCommonIds(headers, context, mockDeps)).toEqual({
81+
ok: true,
82+
value: {
83+
correlationId: 'CORR123',
84+
supplierId: 'SUP456'
85+
}
86+
});
87+
});
88+
89+
it('refuses to use the supplier id from the header if authorizer is present', () => {
90+
const headers = { 'x-correlation-id': 'CORR123', 'x-supplier-id': 'SUP123', 'x-request-id': 'REQ123' };
91+
const context = { 'authorizer': {}} as unknown as APIGatewayProxyEvent['requestContext'];
92+
expect(extractCommonIds(headers, context, mockDeps)).toEqual({
93+
ok: false,
94+
error: expect.any(Error),
95+
correlationId: 'CORR123'
96+
});
97+
});
98+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { APIGatewayProxyEvent, APIGatewayProxyEventHeaders } from 'aws-lambda';
2+
import { Deps } from '../config/deps';
3+
import { lowerCaseKeys } from './validation';
4+
5+
6+
export function extractCommonIds(headers: APIGatewayProxyEventHeaders, context: APIGatewayProxyEvent['requestContext'], deps: Deps
7+
): { ok: true; value: { correlationId: string; supplierId: string; }; } | { ok: false; error: Error; correlationId?: string; } {
8+
9+
if (!headers || Object.keys(headers).length === 0) {
10+
return { ok: false, error: new Error('The request headers are empty') };
11+
}
12+
13+
const lowerCasedHeaders = lowerCaseKeys(headers);
14+
15+
const correlationId = lowerCasedHeaders[deps.env.APIM_CORRELATION_HEADER];
16+
if (!correlationId) {
17+
return { ok: false, error: new Error("The request headers don't contain the APIM correlation id") };
18+
}
19+
20+
const requestId = lowerCasedHeaders['x-request-id'];
21+
if (!requestId) {
22+
return {
23+
ok: false,
24+
error: new Error("The request headers don't contain the x-request-id"),
25+
correlationId
26+
};
27+
}
28+
29+
// In normal API usage, we expect the authorizer to provide the supplier ID. When the lambda is invoked directly, for instance
30+
// in the AWS console, then fall back to using the header.
31+
32+
const supplierId = context.authorizer?
33+
context.authorizer.principalId:
34+
lowerCasedHeaders[deps.env.SUPPLIER_ID_HEADER];
35+
36+
if (!supplierId) {
37+
return {
38+
ok: false,
39+
error: new Error('The supplier ID is missing from the request'),
40+
correlationId
41+
};
42+
}
43+
44+
return { ok: true, value: { correlationId, supplierId } };
45+
}

0 commit comments

Comments
 (0)