Skip to content

Commit 680ae81

Browse files
committed
Removed Cloudwatch client
1 parent 02802bf commit 680ae81

File tree

4 files changed

+149
-152
lines changed

4 files changed

+149
-152
lines changed

lambdas/authorizer/package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
{
22
"dependencies": {
3-
"@aws-sdk/client-cloudwatch": "^3.922.0",
4-
"esbuild": "^0.25.11",
5-
"pino": "^10.1.0",
6-
"zod": "^4.1.12"
3+
"esbuild": "^0.25.11"
74
},
85
"devDependencies": {
96
"@tsconfig/node22": "^22.0.2",

lambdas/authorizer/src/__tests__/index.test.ts

Lines changed: 118 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ const mockedDeps: jest.Mocked<Deps> = {
1111
CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS: 14,
1212
APIM_APPLICATION_ID_HEADER: 'apim-application-id'
1313
} as unknown as EnvVars,
14-
cloudWatchClient: {
15-
send: jest.fn().mockResolvedValue({}),
16-
} as any,
1714
supplierRepo: {
1815
getSupplierByApimId: jest.fn(),
1916
} as any,
@@ -49,49 +46,48 @@ describe('Authorizer Lambda Function', () => {
4946
jest.useRealTimers();
5047
})
5148

52-
// it('Should not send CloudWatch metric when certificate is null', async () => {
53-
// mockEvent.requestContext.identity.clientCert = null;
49+
it('Should not log CloudWatch metric when certificate is null', async () => {
50+
mockEvent.requestContext.identity.clientCert = null;
5451

55-
// const handler = createAuthorizerHandler(mockedDeps);
56-
// handler(mockEvent, mockContext, mockCallback);
57-
// await new Promise(process.nextTick);
52+
const handler = createAuthorizerHandler(mockedDeps);
53+
handler(mockEvent, mockContext, mockCallback);
54+
await new Promise(process.nextTick);
5855

59-
// expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled();
60-
// });
56+
const mockedInfo = mockedDeps.logger.info as jest.Mock;
57+
expect(mockedInfo.mock.calls).not.toContainEqual(
58+
expect.stringContaining('CloudWatchMetrics'));
59+
});
6160

62-
it('Should send CloudWatch metric when the certificate expiry threshold is reached', async () => {
61+
it('Should log CloudWatch metric when the certificate expiry threshold is reached', async () => {
6362
mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-17T14:19:00Z');
6463

6564
const handler = createAuthorizerHandler(mockedDeps);
6665
handler(mockEvent, mockContext, mockCallback);
6766
await new Promise(process.nextTick);
6867

69-
expect(mockedDeps.cloudWatchClient.send).toHaveBeenCalledWith(
70-
expect.objectContaining({
71-
input: {
68+
const mockedInfo = mockedDeps.logger.info as jest.Mock;
69+
expect(mockedInfo.mock.calls.map(call => call[0])).toContain(JSON.stringify(
70+
{_aws: {Timestamp: 1762179540000,
71+
CloudWatchMetrics: [{
7272
Namespace: 'cloudwatch-namespace',
73-
MetricData: [
74-
{
75-
MetricName: 'apim-client-certificate-near-expiry',
76-
Dimensions: [
77-
{ Name: 'SUBJECT_DN', Value: 'CN=test-subject' },
78-
{ Name: 'NOT_AFTER', Value: '2025-11-17T14:19:00Z' },
79-
],
80-
},
81-
],
82-
},
83-
})
84-
);
73+
Dimensions: ['SUBJECT_DN', 'NOT_AFTER'],
74+
Metrics: [{Name: 'apim-client-certificate-near-expiry', Unit: 'Count', Value: 1}]
75+
}]},
76+
SUBJECT_DN: 'CN=test-subject',
77+
NOT_AFTER: '2025-11-17T14:19:00Z',
78+
'apim-client-certificate-near-expiry': 1}));
8579
});
8680

87-
it('Should not send CloudWatch metric when the certificate expiry threshold is not yet reached', async () => {
81+
it('Should not log CloudWatch metric when the certificate expiry threshold is not yet reached', async () => {
8882
mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-18T14:19:00Z');
8983

9084
const handler = createAuthorizerHandler(mockedDeps);
9185
handler(mockEvent, mockContext, mockCallback);
9286
await new Promise(process.nextTick);
9387

94-
expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled();
88+
const mockedInfo = mockedDeps.logger.info as jest.Mock;
89+
expect(mockedInfo.mock.calls).not.toContainEqual(
90+
expect.stringContaining('CloudWatchMetrics'));
9591
});
9692
});
9793

@@ -105,110 +101,110 @@ describe('Authorizer Lambda Function', () => {
105101
} as APIGatewayEventClientCertificate;
106102
}
107103

108-
describe('Supplier ID lookup', () => {
104+
describe('Supplier ID lookup', () => {
109105

110-
it('Should deny the request when no headers are present', async () => {
111-
mockEvent.headers = null;
106+
it('Should deny the request when no headers are present', async () => {
107+
mockEvent.headers = null;
112108

113-
const handler = createAuthorizerHandler(mockedDeps);
114-
handler(mockEvent, mockContext, mockCallback);
115-
await new Promise(process.nextTick);
109+
const handler = createAuthorizerHandler(mockedDeps);
110+
handler(mockEvent, mockContext, mockCallback);
111+
await new Promise(process.nextTick);
116112

117-
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
118-
policyDocument: expect.objectContaining({
119-
Statement: [
120-
expect.objectContaining({
121-
Effect: 'Deny',
122-
}),
123-
],
124-
}),
125-
}));
126-
});
113+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
114+
policyDocument: expect.objectContaining({
115+
Statement: [
116+
expect.objectContaining({
117+
Effect: 'Deny',
118+
}),
119+
],
120+
}),
121+
}));
122+
});
127123

128-
it('Should deny the request when the APIM application ID header is absent', async () => {
129-
mockEvent.headers = {'x-apim-correlation-id': 'correlation-id'};
124+
it('Should deny the request when the APIM application ID header is absent', async () => {
125+
mockEvent.headers = {'x-apim-correlation-id': 'correlation-id'};
130126

131-
const handler = createAuthorizerHandler(mockedDeps);
132-
handler(mockEvent, mockContext, mockCallback);
133-
await new Promise(process.nextTick);
127+
const handler = createAuthorizerHandler(mockedDeps);
128+
handler(mockEvent, mockContext, mockCallback);
129+
await new Promise(process.nextTick);
134130

135-
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
136-
policyDocument: expect.objectContaining({
137-
Statement: [
138-
expect.objectContaining({
139-
Effect: 'Deny',
140-
}),
141-
],
142-
}),
143-
}));
144-
});
131+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
132+
policyDocument: expect.objectContaining({
133+
Statement: [
134+
expect.objectContaining({
135+
Effect: 'Deny',
136+
}),
137+
],
138+
}),
139+
}));
140+
});
145141

146-
it('Should deny the request when no supplier ID is found', async () => {
147-
mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' };
148-
(mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockRejectedValue(new Error('Supplier not found'));
149-
150-
const handler = createAuthorizerHandler(mockedDeps);
151-
handler(mockEvent, mockContext, mockCallback);
152-
await new Promise(process.nextTick);
153-
154-
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
155-
policyDocument: expect.objectContaining({
156-
Statement: [
157-
expect.objectContaining({
158-
Effect: 'Deny',
159-
}),
160-
],
161-
}),
162-
}));
163-
});
142+
it('Should deny the request when no supplier ID is found', async () => {
143+
mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' };
144+
(mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockRejectedValue(new Error('Supplier not found'));
164145

165-
it('Should allow the request when the supplier ID is found', async () => {
166-
mockEvent.headers = { 'apim-application-id': 'valid-apim-id' };
167-
(mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({
168-
id: 'supplier-123',
169-
apimApplicationId: 'valid-apim-id',
170-
name: 'Test Supplier',
171-
status: 'ENABLED'
172-
});
173-
174-
const handler = createAuthorizerHandler(mockedDeps);
175-
handler(mockEvent, mockContext, mockCallback);
176-
await new Promise(process.nextTick);
177-
178-
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
179-
policyDocument: expect.objectContaining({
180-
Statement: [
181-
expect.objectContaining({
182-
Effect: 'Allow',
183-
}),
184-
],
185-
}),
186-
principalId: 'supplier-123',
187-
}));
146+
const handler = createAuthorizerHandler(mockedDeps);
147+
handler(mockEvent, mockContext, mockCallback);
148+
await new Promise(process.nextTick);
149+
150+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
151+
policyDocument: expect.objectContaining({
152+
Statement: [
153+
expect.objectContaining({
154+
Effect: 'Deny',
155+
}),
156+
],
157+
}),
158+
}));
159+
});
160+
161+
it('Should allow the request when the supplier ID is found', async () => {
162+
mockEvent.headers = { 'apim-application-id': 'valid-apim-id' };
163+
(mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({
164+
id: 'supplier-123',
165+
apimApplicationId: 'valid-apim-id',
166+
name: 'Test Supplier',
167+
status: 'ENABLED'
188168
});
169+
170+
const handler = createAuthorizerHandler(mockedDeps);
171+
handler(mockEvent, mockContext, mockCallback);
172+
await new Promise(process.nextTick);
173+
174+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
175+
policyDocument: expect.objectContaining({
176+
Statement: [
177+
expect.objectContaining({
178+
Effect: 'Allow',
179+
}),
180+
],
181+
}),
182+
principalId: 'supplier-123',
183+
}));
189184
});
185+
});
190186

191-
it('Should deny the request the supplier is disabled', async () => {
192-
mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' };
193-
(mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({
194-
id: 'supplier-123',
195-
apimApplicationId: 'valid-apim-id',
196-
name: 'Test Supplier',
197-
status: 'DISABLED'
198-
});
199-
200-
const handler = createAuthorizerHandler(mockedDeps);
201-
handler(mockEvent, mockContext, mockCallback);
202-
await new Promise(process.nextTick);
203-
204-
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
205-
policyDocument: expect.objectContaining({
206-
Statement: [
207-
expect.objectContaining({
208-
Effect: 'Deny',
209-
}),
210-
],
187+
it('Should deny the request the supplier is disabled', async () => {
188+
mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' };
189+
(mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({
190+
id: 'supplier-123',
191+
apimApplicationId: 'valid-apim-id',
192+
name: 'Test Supplier',
193+
status: 'DISABLED'
194+
});
195+
196+
const handler = createAuthorizerHandler(mockedDeps);
197+
handler(mockEvent, mockContext, mockCallback);
198+
await new Promise(process.nextTick);
199+
200+
expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({
201+
policyDocument: expect.objectContaining({
202+
Statement: [
203+
expect.objectContaining({
204+
Effect: 'Deny',
211205
}),
212-
}));
213-
});
206+
],
207+
}),
208+
}));
209+
});
214210
});

lambdas/authorizer/src/authorizer.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import {APIGatewayAuthorizerResult, APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerEventHeaders, APIGatewayRequestAuthorizerHandler,
1515
Callback, Context } from 'aws-lambda';
1616
import { Deps } from './deps';
17-
import { PutMetricDataCommand } from '@aws-sdk/client-cloudwatch';
1817
import { Supplier } from '@internal/datastore';
1918

2019
export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizerHandler {
@@ -27,8 +26,9 @@ export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizer
2726
deps.logger.info(event, 'Received event');
2827

2928

30-
checkCertificateExpiry(event.requestContext.identity.clientCert, deps)
31-
.then(() => getSupplier(event.headers, deps))
29+
checkCertificateExpiry(event.requestContext.identity.clientCert, deps);
30+
31+
getSupplier(event.headers, deps)
3232
.then((supplier: Supplier) => {
3333
deps.logger.info('Allow event');
3434
callback(null, generateAllow(event.methodArn, supplier.id));
@@ -100,31 +100,38 @@ async function checkCertificateExpiry(certificate: APIGatewayEventClientCertific
100100
validity: certificate?.validity,
101101
});
102102

103-
certificate = certificate || {subjectDN: 'CN=123', validity: {notAfter: '2025-11-06T12:00:00Z'}} as unknown as APIGatewayEventClientCertificate;
103+
if (!certificate) {
104+
// In a real production environment, we won't have got this far if there wasn't a cert
105+
return;
106+
}
104107

105108
const expiry = getCertificateExpiryInDays(certificate);
106109

107110
if (expiry <= deps.env.CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS) {
108-
const { subjectDN, validity } = certificate;
109-
110-
deps.logger.info({
111-
description: 'Client certificate near expiry',
112-
certificateExpiry: validity.notAfter,
113-
subjectDN,
114-
});
115-
await deps.cloudWatchClient.send(buildCloudWatchCommand(deps.env.CLOUDWATCH_NAMESPACE, certificate));
111+
deps.logger.info(JSON.stringify(buildCloudWatchMetric(deps.env.CLOUDWATCH_NAMESPACE, certificate)));
116112
}
117113

118-
function buildCloudWatchCommand(namespace: string, certificate: APIGatewayEventClientCertificate): PutMetricDataCommand {
119-
return new PutMetricDataCommand({
120-
MetricData: [{
121-
MetricName: 'apim-client-certificate-near-expiry',
122-
Dimensions: [
123-
{Name: 'SUBJECT_DN', Value: certificate.subjectDN},
124-
{Name: 'NOT_AFTER', Value: certificate.validity.notAfter}
125-
]
126-
}],
127-
Namespace: namespace
128-
});
114+
function buildCloudWatchMetric(namespace: string, certificate: APIGatewayEventClientCertificate) {
115+
return {
116+
_aws: {
117+
Timestamp: new Date().valueOf(),
118+
CloudWatchMetrics: [
119+
{
120+
Namespace: namespace,
121+
Dimensions: ['SUBJECT_DN', 'NOT_AFTER'],
122+
Metrics: [
123+
{
124+
Name: 'apim-client-certificate-near-expiry',
125+
Unit: 'Count',
126+
Value: 1,
127+
},
128+
],
129+
},
130+
],
131+
},
132+
'SUBJECT_DN': certificate.subjectDN,
133+
'NOT_AFTER': certificate.validity.notAfter,
134+
'apim-client-certificate-near-expiry': 1,
135+
};
129136
}
130137
};

lambdas/authorizer/src/deps.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { CloudWatchClient } from "@aws-sdk/client-cloudwatch";
21
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
32
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
43
import pino from 'pino';
@@ -7,7 +6,6 @@ import { SupplierRepository } from '@internal/datastore';
76

87
export type Deps = {
98
supplierRepo: SupplierRepository;
10-
cloudWatchClient: CloudWatchClient;
119
logger: pino.Logger;
1210
env: EnvVars;
1311
};
@@ -30,7 +28,6 @@ export function createDependenciesContainer(): Deps {
3028

3129
return {
3230
supplierRepo: createSupplierRepository(createDocumentClient(), log, envVars),
33-
cloudWatchClient: new CloudWatchClient({}),
3431
logger: log,
3532
env: envVars
3633
};

0 commit comments

Comments
 (0)