-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
OLH-1053 Update reported flag when activity is marked as suspicious
Create a new lambda that subscribes to the SuspiciousActivity SNS topic. When triggered, the lambda will update the record for that activity (in the activity_log table), setting reported_suspicious to true.
- Loading branch information
Showing
4 changed files
with
465 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { SNSEvent } from "aws-lambda"; | ||
import { | ||
SendMessageCommand, | ||
SendMessageCommandOutput, | ||
SendMessageRequest, | ||
SQSClient, | ||
} from "@aws-sdk/client-sqs"; | ||
import { | ||
DynamoDBDocumentClient, | ||
UpdateCommand, | ||
QueryCommand, | ||
} from "@aws-sdk/lib-dynamodb"; | ||
import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; | ||
import {ActivityLogEntry} from './common/model' | ||
|
||
const dynamoClient = new DynamoDBClient({}); | ||
const dynamoDocClient = DynamoDBDocumentClient.from(dynamoClient); | ||
|
||
export const sendSqsMessage = async ( | ||
messageBody: string, | ||
queueUrl: string | undefined | ||
): Promise<SendMessageCommandOutput> => { | ||
const { AWS_REGION } = process.env; | ||
const client = new SQSClient({ region: AWS_REGION }); | ||
const message: SendMessageRequest = { | ||
QueueUrl: queueUrl, | ||
MessageBody: messageBody, | ||
}; | ||
return client.send(new SendMessageCommand(message)); | ||
}; | ||
|
||
export const getItemByEventId = async ( | ||
tableName: string, | ||
indexName: string, | ||
eventId: string | ||
): Promise<{ user_id: string; timestamp: number }> => { | ||
const getItem = new QueryCommand({ | ||
TableName: tableName, | ||
IndexName: indexName, | ||
KeyConditionExpression: "event_id = :event_id", | ||
ExpressionAttributeValues: { | ||
":event_id": eventId, | ||
}, | ||
}); | ||
|
||
const result = await dynamoDocClient.send(getItem); | ||
|
||
if (result?.Items?.length !== 1) { | ||
throw Error( | ||
`Expecting exactly 1 result from getItemByEventId, but got ${result?.Items?.length}` | ||
); | ||
} | ||
const item = result.Items[0]; | ||
return { user_id: item.user_id, timestamp: item.timestamp }; | ||
}; | ||
|
||
export const markEventAsReported = async ( | ||
tableName: string, | ||
user_id: string, | ||
timestamp: number | ||
) => { | ||
const command = new UpdateCommand({ | ||
TableName: tableName, | ||
Key: { | ||
user_id, | ||
timestamp, | ||
}, | ||
UpdateExpression: "set reported_suspicious = :reported_suspicious", | ||
ExpressionAttributeValues: { | ||
":reported_suspicious": true, | ||
}, | ||
}); | ||
|
||
return dynamoDocClient.send(command); | ||
}; | ||
|
||
export const handler = async (event: SNSEvent): Promise<void> => { | ||
const { DLQ_URL, TABLE_NAME, INDEX_NAME } = process.env; | ||
await Promise.all( | ||
event.Records.map(async (record) => { | ||
try { | ||
if (!TABLE_NAME) { | ||
throw new Error( | ||
"Cannot handle event as table name has not been provided in the environment" | ||
); | ||
} | ||
if (!INDEX_NAME) { | ||
throw new Error( | ||
"Cannot handle event as index name has not been provided in the environment" | ||
); | ||
} | ||
const receivedEvent: ActivityLogEntry = JSON.parse(record.Sns.Message); | ||
|
||
const { user_id, timestamp } = await getItemByEventId( | ||
TABLE_NAME, | ||
INDEX_NAME, | ||
receivedEvent.event_id | ||
); | ||
await markEventAsReported(TABLE_NAME, user_id, timestamp); | ||
} catch (err) { | ||
const response = await sendSqsMessage(record.Sns.Message, DLQ_URL); | ||
console.error( | ||
`[Message sent to DLQ] with message id = ${response.MessageId}`, | ||
err as Error | ||
); | ||
} | ||
}) | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import "aws-sdk-client-mock-jest"; | ||
import { | ||
getItemByEventId, | ||
handler, | ||
markEventAsReported, | ||
sendSqsMessage, | ||
} from "../mark-suspicious-activity-as-reported"; | ||
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; | ||
import { mockClient } from "aws-sdk-client-mock"; | ||
import { | ||
DynamoDBDocumentClient, | ||
QueryCommand, | ||
UpdateCommand, | ||
} from "@aws-sdk/lib-dynamodb"; | ||
import { | ||
TEST_SNS_EVENT_WITH_EVENT, | ||
queueUrl, | ||
eventId, | ||
indexName, | ||
tableName, | ||
timestamp, | ||
userId, | ||
} from "./testFixtures"; | ||
|
||
const sqsMock = mockClient(SQSClient); | ||
const dynamoMock = mockClient(DynamoDBDocumentClient); | ||
|
||
describe("getItemByEventId", () => { | ||
beforeEach(() => { | ||
dynamoMock.reset(); | ||
dynamoMock.on(QueryCommand).resolves({ | ||
Items: [{ user_id: userId, timestamp: timestamp }], | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test("correctly retreives the event from the datastore", async () => { | ||
await getItemByEventId(tableName, indexName, eventId); | ||
expect(dynamoMock.commandCalls(QueryCommand).length).toEqual(1); | ||
expect(dynamoMock).toHaveReceivedCommandWith(QueryCommand, { | ||
TableName: tableName, | ||
IndexName: indexName, | ||
ExpressionAttributeValues: { | ||
":event_id": eventId, | ||
}, | ||
}); | ||
}); | ||
|
||
test("returns the user id and timestamp", async () => { | ||
const response = await getItemByEventId(tableName, indexName, eventId); | ||
expect(response).toEqual({ user_id: userId, timestamp: timestamp }); | ||
}); | ||
}); | ||
|
||
describe("markEventAsReported", () => { | ||
beforeEach(() => { | ||
dynamoMock.reset(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test("updates the correct event as reported", async () => { | ||
await markEventAsReported(tableName, userId, timestamp); | ||
expect(dynamoMock.commandCalls(UpdateCommand).length).toEqual(1); | ||
expect(dynamoMock).toHaveReceivedCommandWith(UpdateCommand, { | ||
TableName: tableName, | ||
Key: { | ||
user_id: userId, | ||
timestamp: timestamp, | ||
}, | ||
UpdateExpression: "set reported_suspicious = :reported_suspicious", | ||
ExpressionAttributeValues: { | ||
":reported_suspicious": true, | ||
}, | ||
}); | ||
}); | ||
}); | ||
|
||
describe("handler", () => { | ||
const OLD_ENV = process.env; | ||
|
||
beforeEach(() => { | ||
jest.resetModules(); | ||
|
||
process.env = { ...OLD_ENV }; | ||
process.env.TABLE_NAME = tableName; | ||
process.env.DLQ_URL = queueUrl; | ||
process.env.INDEX_NAME = indexName; | ||
|
||
dynamoMock.reset(); | ||
|
||
dynamoMock.on(QueryCommand).resolves({ | ||
Items: [{ user_id: userId, timestamp: timestamp }], | ||
}); | ||
|
||
sqsMock.on(SendMessageCommand).resolves({ MessageId: "MessageId" }); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
process.env = OLD_ENV; | ||
}); | ||
|
||
test("the handler makes the correct queries", async () => { | ||
await handler(TEST_SNS_EVENT_WITH_EVENT); | ||
expect(dynamoMock.commandCalls(QueryCommand).length).toEqual(1); | ||
expect(dynamoMock.commandCalls(UpdateCommand).length).toEqual(1); | ||
|
||
expect(dynamoMock).toHaveReceivedCommandWith(QueryCommand, { | ||
TableName: tableName, | ||
IndexName: indexName, | ||
KeyConditionExpression: "event_id = :event_id", | ||
ExpressionAttributeValues: { | ||
":event_id": eventId, | ||
}, | ||
}); | ||
|
||
expect(dynamoMock).toHaveReceivedCommandWith(UpdateCommand, { | ||
TableName: tableName, | ||
Key: { | ||
user_id: userId, | ||
timestamp: timestamp, | ||
}, | ||
UpdateExpression: "set reported_suspicious = :reported_suspicious", | ||
ExpressionAttributeValues: { | ||
":reported_suspicious": true, | ||
}, | ||
}); | ||
}); | ||
|
||
test("the handler sends to DLQ if there is an error", async () => { | ||
process.env.TABLE_NAME = undefined; | ||
await handler(TEST_SNS_EVENT_WITH_EVENT); | ||
expect(sqsMock.commandCalls(SendMessageCommand).length).toEqual(1); | ||
}); | ||
}); | ||
|
||
describe("sendSQSMessage", () => { | ||
beforeEach(() => { | ||
sqsMock.reset(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
test("send sqs successfully", async () => { | ||
const txMAEvent = { | ||
component_id: "https://home.account.gov.uk", | ||
event_name: "HOME_REPORT_SUSPICIOUS_ACTIVITY", | ||
extensions: { | ||
reported_session_id: "111111", | ||
}, | ||
user: { | ||
persistent_session_id: "111111", | ||
session_id: "111112", | ||
user_id: "1234567", | ||
}, | ||
}; | ||
await sendSqsMessage(JSON.stringify(txMAEvent), "TXMA_QUEUE_URL"); | ||
expect(sqsMock.commandCalls(SendMessageCommand).length).toEqual(1); | ||
expect(sqsMock).toHaveReceivedCommandWith(SendMessageCommand, { | ||
QueueUrl: "TXMA_QUEUE_URL", | ||
MessageBody: JSON.stringify(txMAEvent), | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.