Skip to content

Commit

Permalink
OLH-1053 Update reported flag when activity is marked as suspicious
Browse files Browse the repository at this point in the history
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
saralk committed Feb 1, 2024
1 parent 457378e commit a196aac
Show file tree
Hide file tree
Showing 4 changed files with 465 additions and 9 deletions.
109 changes: 109 additions & 0 deletions src/mark-suspicious-activity-as-reported.ts
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'

Check failure on line 14 in src/mark-suspicious-activity-as-reported.ts

View workflow job for this annotation

GitHub Actions / di-account-management-backend

Replace `ActivityLogEntry}·from·'./common/model'` with `·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
);
}
})
);
};
172 changes: 172 additions & 0 deletions src/tests/mark-suspicious-activity-as-reported.test.ts
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),
});
});
});
47 changes: 38 additions & 9 deletions src/tests/testFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,23 @@ export const randomEventType = "AUTH_OTHER_RANDOM_EVENT";
export const queueUrl = "http://my_queue_url";
export const messageId = "MyMessageId";
export const tableName = "tableName";
export const indexName = "indexName";

export const user: UserData = {
user_id: userId,
session_id: sessionId,
};

export const TEST_ACTIVITY_LOG_ENTRY: ActivityLogEntry = {
event_type: eventType,
session_id: sessionId,
user_id: userId,
timestamp,
event_id: eventId,
client_id: clientId,
reported_suspicious: reportedSuspicious,
};

export const TEST_USER_DATA: UserData = {
user_id: "user-id",
access_token: "access_token",
Expand Down Expand Up @@ -67,13 +79,34 @@ export const TEST_SNS_MESSAGE: SNSMessage = {
Subject: "Subject",
};

export const TEST_SNS_EVENT_MESSAGE: SNSMessage = {
SignatureVersion: "SignatureVersion",
Timestamp: "Timestamp",
Signature: "Signature",
SigningCertUrl: "SigningCertUrl",
MessageId: "MessageId",
Message: JSON.stringify(TEST_ACTIVITY_LOG_ENTRY),
MessageAttributes: {},
Type: "Type",
UnsubscribeUrl: "unsubscribeUrl",
TopicArn: "TopicArn",
Subject: "Subject",
};

export const TEST_SNS_EVENT_RECORD: SNSEventRecord = {
EventVersion: "1",
EventSubscriptionArn: "arn",
EventSource: "source",
Sns: TEST_SNS_MESSAGE,
};

export const TEST_SNS_EVENT_RECORD_WITH_EVENT: SNSEventRecord = {
EventVersion: "1",
EventSubscriptionArn: "arn",
EventSource: "source",
Sns: TEST_SNS_EVENT_MESSAGE,
};

export const TEST_SNS_EVENT: SNSEvent = {
Records: [TEST_SNS_EVENT_RECORD],
};
Expand All @@ -82,15 +115,11 @@ export const TEST_SNS_EVENT_WITH_TWO_RECORDS: SNSEvent = {
Records: [TEST_SNS_EVENT_RECORD, TEST_SNS_EVENT_RECORD],
};

export const TEST_ACTIVITY_LOG_ENTRY: ActivityLogEntry = {
event_type: eventType,
session_id: sessionId,
user_id: userId,
timestamp,
event_id: eventId,
client_id: clientId,
reported_suspicious: reportedSuspicious,
};
export const TEST_SNS_EVENT_WITH_EVENT: SNSEvent = {
Records: [TEST_SNS_EVENT_RECORD_WITH_EVENT],
}

Check failure on line 120 in src/tests/testFixtures.ts

View workflow job for this annotation

GitHub Actions / di-account-management-backend

Replace `⏎⏎` with `;`



const NO_ACTIVITY_ARRAY = { ...TEST_ACTIVITY_LOG_ENTRY, activities: undefined };
export const ACTIVITY_LOG_ENTRY_NO_ACTIVITY_ARRAY: ActivityLogEntry =
Expand Down
Loading

0 comments on commit a196aac

Please sign in to comment.