feat: Cleanup api keys and webhooks for suspended users (#3756)
This commit is contained in:
102
server/queues/tasks/CleanupDemotedUserTask.test.ts
Normal file
102
server/queues/tasks/CleanupDemotedUserTask.test.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { ApiKey } from "@server/models";
|
||||
import {
|
||||
buildUser,
|
||||
buildApiKey,
|
||||
buildAdmin,
|
||||
buildWebhookSubscription,
|
||||
buildViewer,
|
||||
} from "@server/test/factories";
|
||||
import { flushdb } from "@server/test/support";
|
||||
import CleanupDemotedUserTask from "./CleanupDemotedUserTask";
|
||||
|
||||
beforeEach(() => flushdb());
|
||||
|
||||
describe("CleanupDemotedUserTask", () => {
|
||||
it("should delete api keys for suspended user", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
suspendedAt: new Date(),
|
||||
suspendedById: admin.id,
|
||||
});
|
||||
const apiKey = await buildApiKey({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const task = new CleanupDemotedUserTask();
|
||||
await task.perform({ userId: user.id });
|
||||
expect(await ApiKey.findByPk(apiKey.id)).toBeNull();
|
||||
});
|
||||
|
||||
it("should delete api keys for viewer", async () => {
|
||||
const user = await buildViewer();
|
||||
const apiKey = await buildApiKey({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const task = new CleanupDemotedUserTask();
|
||||
await task.perform({ userId: user.id });
|
||||
expect(await ApiKey.findByPk(apiKey.id)).toBeNull();
|
||||
});
|
||||
|
||||
it("should retain api keys for member", async () => {
|
||||
const user = await buildUser();
|
||||
const apiKey = await buildApiKey({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
const task = new CleanupDemotedUserTask();
|
||||
await task.perform({ userId: user.id });
|
||||
expect(await ApiKey.findByPk(apiKey.id)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should disable webhooks for suspended user", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
suspendedAt: new Date(),
|
||||
suspendedById: admin.id,
|
||||
});
|
||||
const webhook = await buildWebhookSubscription({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
});
|
||||
|
||||
const task = new CleanupDemotedUserTask();
|
||||
await task.perform({ userId: user.id });
|
||||
|
||||
await webhook.reload();
|
||||
expect(webhook.enabled).toEqual(false);
|
||||
});
|
||||
|
||||
it("should disable webhooks for member", async () => {
|
||||
const admin = await buildAdmin();
|
||||
const user = await buildUser({
|
||||
teamId: admin.teamId,
|
||||
});
|
||||
const webhook = await buildWebhookSubscription({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
});
|
||||
|
||||
const task = new CleanupDemotedUserTask();
|
||||
await task.perform({ userId: user.id });
|
||||
|
||||
await webhook.reload();
|
||||
expect(webhook.enabled).toEqual(false);
|
||||
});
|
||||
|
||||
it("should retain webhooks for admin", async () => {
|
||||
const user = await buildAdmin();
|
||||
const webhook = await buildWebhookSubscription({
|
||||
teamId: user.teamId,
|
||||
createdById: user.id,
|
||||
});
|
||||
|
||||
const task = new CleanupDemotedUserTask();
|
||||
await task.perform({ userId: user.id });
|
||||
|
||||
await webhook.reload();
|
||||
expect(webhook.enabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
57
server/queues/tasks/CleanupDemotedUserTask.ts
Normal file
57
server/queues/tasks/CleanupDemotedUserTask.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { sequelize } from "@server/database/sequelize";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { WebhookSubscription, ApiKey, User } from "@server/models";
|
||||
import BaseTask from "./BaseTask";
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Task to disable mechanisms for exporting data from a suspended or demoted user,
|
||||
* currently this is done by destroying associated Api Keys and disabling webhooks.
|
||||
*/
|
||||
export default class CleanupDemotedUserTask extends BaseTask<Props> {
|
||||
public async perform(props: Props) {
|
||||
await sequelize.transaction(async (transaction) => {
|
||||
const user = await User.findByPk(props.userId, { rejectOnEmpty: true });
|
||||
|
||||
if (user.isSuspended || !user.isAdmin) {
|
||||
const subscriptions = await WebhookSubscription.findAll({
|
||||
where: {
|
||||
createdById: props.userId,
|
||||
enabled: true,
|
||||
},
|
||||
transaction,
|
||||
lock: transaction.LOCK.UPDATE,
|
||||
});
|
||||
await Promise.all(
|
||||
subscriptions.map((subscription) =>
|
||||
subscription.disable({ transaction })
|
||||
)
|
||||
);
|
||||
Logger.info(
|
||||
"task",
|
||||
`Disabled ${subscriptions.length} webhooks for user ${props.userId}`
|
||||
);
|
||||
}
|
||||
|
||||
if (user.isSuspended || user.isViewer) {
|
||||
const apiKeys = await ApiKey.findAll({
|
||||
where: {
|
||||
userId: props.userId,
|
||||
},
|
||||
transaction,
|
||||
lock: transaction.LOCK.UPDATE,
|
||||
});
|
||||
await Promise.all(
|
||||
apiKeys.map((apiKey) => apiKey.destroy({ transaction }))
|
||||
);
|
||||
Logger.info(
|
||||
"task",
|
||||
`Destroyed ${apiKeys.length} api keys for user ${props.userId}`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -586,7 +586,7 @@ export default class DeliverWebhookTask extends BaseTask<Props> {
|
||||
|
||||
if (recentDeliveries.length === 25 && allFailed) {
|
||||
// If the last 25 deliveries failed, disable the subscription
|
||||
await subscription.update({ enabled: false });
|
||||
await subscription.disable();
|
||||
|
||||
// Send an email to the creator of the webhook to let them know
|
||||
const [createdBy, team] = await Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user