Skip to content

Commit

Permalink
feat(firewall): handle firewall with multiple events involved
Browse files Browse the repository at this point in the history
If the firewall catches some suspicious activity from an IP address and that IP address continues to
perform the same action, new events are created for the same firewall side-effect.
  • Loading branch information
Rafatcb committed Jul 21, 2024
1 parent 37764a0 commit 9ae0ce4
Show file tree
Hide file tree
Showing 7 changed files with 937 additions and 53 deletions.
3 changes: 2 additions & 1 deletion models/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@ async function creditOrDebitTabCoins(oldContent, newContent, options = {}) {
// or if it was deleted and is catch by firewall: "deleted" -> "firewall".
if (
oldContent &&
((!oldContent.published_at && newContent.status !== 'published') || oldContent.status === 'deleted')
((!oldContent.published_at && newContent.status !== 'published') ||
['deleted', 'firewall'].includes(oldContent.status))
) {
return;
}
Expand Down
20 changes: 0 additions & 20 deletions models/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,6 @@ async function findAllRelatedEvents(id) {
return results.rows;
}

async function findOneById(eventId) {
const query = {
text: `
SELECT
*
FROM
events
WHERE
id = $1
LIMIT
1
;`,
values: [eventId],
};

const results = await database.query(query);
return results.rows[0];
}

async function findOneByOriginalEventId(originalEventId, where = {}) {
const firstWhereArgumentIndex = 2;
const whereClause = buildWhereClause(where, firstWhereArgumentIndex);
Expand Down Expand Up @@ -136,6 +117,5 @@ function buildWhereClause(where, nextArgumentIndex) {
export default Object.freeze({
create,
findAllRelatedEvents,
findOneById,
findOneByOriginalEventId,
});
18 changes: 5 additions & 13 deletions models/firewall/find.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import user from 'models/user';
import eventTypes from './event-types';

async function findByEventId(eventId) {
const foundOriginalEvent = await event.findOneById(eventId);
const relatedEvents = await event.findAllRelatedEvents(eventId);
const foundRequestedEvent = relatedEvents.find((event) => event.id === eventId);

if (!foundOriginalEvent || !eventTypes.firewall.includes(foundOriginalEvent.type)) {
if (!foundRequestedEvent || !eventTypes.firewall.includes(foundRequestedEvent.type)) {
throw new NotFoundError({
message: `O id "${eventId}" não foi encontrado no sistema.`,
action: 'Verifique se o "id" está digitado corretamente.',
Expand All @@ -18,20 +19,11 @@ async function findByEventId(eventId) {
});
}

const foundReviewedEvent = await event.findOneByOriginalEventId(eventId, {
type: eventTypes.review,
});

const events = [foundOriginalEvent];

if (foundReviewedEvent) {
events.push(foundReviewedEvent);
}
const affectedData = await getAffectedData(events.at(-1));
const affectedData = await getAffectedData(relatedEvents.at(-1));

return {
affected: affectedData,
events: events,
events: relatedEvents,
};
}

Expand Down
40 changes: 21 additions & 19 deletions models/firewall/review.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ const reviewFunctions = {
};

async function reviewEvent({ action, eventId, originatorIp, originatorUserId }) {
const firewallEvent = await validateAndGetFirewallEventToReview(eventId);
const relatedEvents = await validateAndGetRelatedEventsToReview(eventId);

const firewallEvent = relatedEvents.find((e) => e.id === eventId);

const metadata = {
original_event_id: eventId,
Expand All @@ -44,7 +46,7 @@ async function reviewEvent({ action, eventId, originatorIp, originatorUserId })
},
);

const events = [firewallEvent, createdEvent];
const events = [...relatedEvents, createdEvent];

const affected = await reviewFunctions[eventType](firewallEvent, {
transaction,
Expand All @@ -65,33 +67,33 @@ async function reviewEvent({ action, eventId, originatorIp, originatorUserId })
}
}

async function validateAndGetFirewallEventToReview(eventId) {
const reviewingEvent = await event.findOneByOriginalEventId(eventId, {
types: eventTypes.review,
});
async function validateAndGetRelatedEventsToReview(eventId) {
const relatedEvents = await event.findAllRelatedEvents(eventId);

if (reviewingEvent) {
throw new ValidationError({
message: 'Você está tentando analisar um evento que já foi analisado.',
action: 'Utilize um "id" que aponte para um evento de firewall que ainda não foi analisado.',
if (!relatedEvents.length) {
throw new NotFoundError({
message: `O id "${eventId}" não foi encontrado no sistema.`,
action: 'Verifique se o "id" está digitado corretamente.',
stack: new Error().stack,
errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:EVENT_ALREADY_REVIEWED',
errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:NOT_FOUND',
key: 'id',
});
}

const firewallEvent = await event.findOneById(eventId);
const reviewingEvent = relatedEvents.find((e) => eventTypes.review.includes(e.type));

if (!firewallEvent) {
throw new NotFoundError({
message: `O id "${eventId}" não foi encontrado no sistema.`,
action: 'Verifique se o "id" está digitado corretamente.',
if (reviewingEvent) {
throw new ValidationError({
message: 'Você está tentando analisar um evento que já foi analisado.',
action: 'Utilize um "id" que aponte para um evento de firewall que ainda não foi analisado.',
stack: new Error().stack,
errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:NOT_FOUND',
errorLocationCode: 'MODEL:FIREWALL:VALIDATE_AND_GET_FIREWALL_EVENT_TO_REVIEW:EVENT_ALREADY_REVIEWED',
key: 'id',
});
}

const firewallEvent = relatedEvents.find((e) => e.id === eventId);

if (!eventTypes.firewall.includes(firewallEvent.type)) {
throw new ValidationError({
message: 'Você está tentando analisar um evento inválido.',
Expand All @@ -102,7 +104,7 @@ async function validateAndGetFirewallEventToReview(eventId) {
});
}

return firewallEvent;
return relatedEvents;
}

async function confirmBlockUsers(firewallEvent, options) {
Expand Down Expand Up @@ -168,7 +170,7 @@ async function unblockUsers(firewallEvent, options) {
const activeUsers = [];

for (const userData of users) {
if (userData.features.length === 0) {
if (userData.features.length === 0 || (userData.features.length === 1 && userData.features[0] === 'nuked')) {
inactiveUsers.push(userData.id);
} else {
activeUsers.push(userData.id);
Expand Down
194 changes: 194 additions & 0 deletions tests/integration/api/v1/events/firewall/[id]/get.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,90 @@ describe('GET /api/v1/events/firewall/[id]', () => {
});
});

test('With two consecutive "firewall:block_users" events from the same IP', async () => {
const usersRequestBuilder = new RequestBuilder('/api/v1/users');
const firewallRequestBuilder = new RequestBuilder(`/api/v1/events/firewall`);
await firewallRequestBuilder.buildUser({ with: ['read:firewall'] });

// Create users
const { responseBody: user1 } = await usersRequestBuilder.post({
username: 'firstUser',
email: '[email protected]',
password: 'password',
});
const { responseBody: user2 } = await usersRequestBuilder.post({
username: 'secondUser',
email: '[email protected]',
password: 'password',
});
const { response: user3Response } = await usersRequestBuilder.post({
username: 'thirdUser',
email: '[email protected]',
password: 'password',
});

expect(user3Response.status).toBe(429);

const firstFirewallEvent = await orchestrator.getLastEvent();

const { response: user4Response } = await usersRequestBuilder.post({
username: 'fourthUser',
email: '[email protected]',
password: 'password',
});

expect(user4Response.status).toBe(429);

const secondFirewallEvent = await orchestrator.getLastEvent();

expect(firstFirewallEvent.id).not.toBe(secondFirewallEvent.id);

// Get firewall side-effects
const { response: firstFirewallResponse, responseBody: firstFirewallResponseBody } =
await firewallRequestBuilder.get(`/${firstFirewallEvent.id}`);
const { response: secondFirewallResponse, responseBody: secondFirewallResponseBody } =
await firewallRequestBuilder.get(`/${secondFirewallEvent.id}`);

expect.soft(firstFirewallResponse.status).toBe(200);
expect.soft(secondFirewallResponse.status).toBe(200);

await usersRequestBuilder.setUser(user1);
const { responseBody: user1AfterFirewall } = await usersRequestBuilder.get(`/${user1.username}`);

await usersRequestBuilder.setUser(user2);
const { responseBody: user2AfterFirewall } = await usersRequestBuilder.get(`/${user2.username}`);

expect(firstFirewallResponseBody).toStrictEqual({
affected: {
users: [user1AfterFirewall, user2AfterFirewall],
},
events: [
{
created_at: firstFirewallEvent.created_at.toISOString(),
id: firstFirewallEvent.id,
metadata: {
from_rule: 'create:user',
users: [user1AfterFirewall.id, user2AfterFirewall.id],
},
originator_user_id: null,
type: 'firewall:block_users',
},
{
created_at: secondFirewallEvent.created_at.toISOString(),
id: secondFirewallEvent.id,
metadata: {
from_rule: 'create:user',
users: [user1AfterFirewall.id, user2AfterFirewall.id],
},
originator_user_id: null,
type: 'firewall:block_users',
},
],
});

expect(firstFirewallResponseBody).toStrictEqual(secondFirewallResponseBody);
});

test.each([
{
action: 'undo',
Expand Down Expand Up @@ -335,6 +419,116 @@ describe('GET /api/v1/events/firewall/[id]', () => {
});
});

test('With two consecutive "firewall:block_users" events from the same IP reviewed', async () => {
const usersRequestBuilder = new RequestBuilder('/api/v1/users');
const firewallRequestBuilder = new RequestBuilder(`/api/v1/events/firewall`);
const firewallUser = await firewallRequestBuilder.buildUser({ with: ['read:firewall', 'review:firewall'] });

// Create users
const { responseBody: user1 } = await usersRequestBuilder.post({
username: 'firstUser',
email: '[email protected]',
password: 'password',
});
const { responseBody: user2 } = await usersRequestBuilder.post({
username: 'secondUser',
email: '[email protected]',
password: 'password',
});
const { response: user3Response } = await usersRequestBuilder.post({
username: 'thirdUser',
email: '[email protected]',
password: 'password',
});

expect(user3Response.status).toBe(429);

const firstFirewallEvent = await orchestrator.getLastEvent();

const { response: user4Response } = await usersRequestBuilder.post({
username: 'fourthUser',
email: '[email protected]',
password: 'password',
});

expect(user4Response.status).toBe(429);

const secondFirewallEvent = await orchestrator.getLastEvent();

expect(firstFirewallEvent.id).not.toBe(secondFirewallEvent.id);

// Check firewall side-effect
expect(firstFirewallEvent.type).toBe('firewall:block_users');
expect(firstFirewallEvent.metadata.users).toStrictEqual([user1.id, user2.id]);
expect(secondFirewallEvent.type).toBe(firstFirewallEvent.type);
expect(secondFirewallEvent.metadata.users).toStrictEqual(firstFirewallEvent.metadata.users);

// Review firewall side-effect
const reviewFirewallRequestBuilder = new RequestBuilder(
`/api/v1/moderations/review_firewall/${firstFirewallEvent.id}`,
);
await reviewFirewallRequestBuilder.setUser(firewallUser);
const { response: reviewResponse } = await reviewFirewallRequestBuilder.post({ action: 'confirm' });
expect(reviewResponse.status).toBe(200);

// Get reviewed firewall event from first event id
const reviewEvent = await orchestrator.getLastEvent();

const { response: responseFromFirstEvent, responseBody: responseBodyFromFirstEvent } =
await firewallRequestBuilder.get(`/${firstFirewallEvent.id}`);

expect.soft(responseFromFirstEvent.status).toBe(200);

const user1AfterReview = await user.findOneById(user1.id, { withBalance: true });
const user2AfterReview = await user.findOneById(user2.id, { withBalance: true });

expect(responseBodyFromFirstEvent).toStrictEqual({
affected: {
users: [mapUserData(user1AfterReview), mapUserData(user2AfterReview)],
},
events: [
{
created_at: firstFirewallEvent.created_at.toISOString(),
id: firstFirewallEvent.id,
metadata: {
from_rule: 'create:user',
users: [user1.id, user2.id],
},
originator_user_id: null,
type: 'firewall:block_users',
},
{
created_at: secondFirewallEvent.created_at.toISOString(),
id: secondFirewallEvent.id,
metadata: {
from_rule: 'create:user',
users: [user1.id, user2.id],
},
originator_user_id: null,
type: 'firewall:block_users',
},
{
created_at: reviewEvent.created_at.toISOString(),
id: reviewEvent.id,
metadata: {
original_event_id: firstFirewallEvent.id,
users: [user1AfterReview.id, user2AfterReview.id],
},
originator_user_id: firewallUser.id,
type: 'moderation:block_users',
},
],
});

// Get reviewed firewall event from second event id
const { response: responseFromSecondEvent, responseBody: responseBodyFromSecondEvent } =
await firewallRequestBuilder.get(`/${secondFirewallEvent.id}`);

expect(responseFromSecondEvent.status).toBe(200);

expect(responseBodyFromSecondEvent).toStrictEqual(responseBodyFromFirstEvent);
});

test('With a "firewall:block_contents:text_root" event', async () => {
const usersRequestBuilder = new RequestBuilder(`/api/v1/users`);
const firewallRequestBuilder = new RequestBuilder(`/api/v1/events/firewall`);
Expand Down
Loading

0 comments on commit 9ae0ce4

Please sign in to comment.