fix: Add locks to user mutations (#5805)

This commit is contained in:
Tom Moor
2023-09-09 23:26:22 -04:00
committed by GitHub
parent c22ed0c82e
commit 9602d09964
5 changed files with 140 additions and 95 deletions

View File

@@ -1,36 +1,41 @@
import { Transaction } from "sequelize";
import { UserRole } from "@shared/types"; import { UserRole } from "@shared/types";
import { ValidationError } from "@server/errors"; import { ValidationError } from "@server/errors";
import { Event, User } from "@server/models"; import { Event, User } from "@server/models";
import CleanupDemotedUserTask from "@server/queues/tasks/CleanupDemotedUserTask"; import CleanupDemotedUserTask from "@server/queues/tasks/CleanupDemotedUserTask";
import { sequelize } from "@server/storage/database";
type Props = { type Props = {
user: User; user: User;
actorId: string; actorId: string;
to: UserRole; to: UserRole;
transaction?: Transaction;
ip: string; ip: string;
}; };
export default async function userDemoter({ user, actorId, to, ip }: Props) { export default async function userDemoter({
user,
actorId,
to,
transaction,
ip,
}: Props) {
if (user.id === actorId) { if (user.id === actorId) {
throw ValidationError("Unable to demote the current user"); throw ValidationError("Unable to demote the current user");
} }
return sequelize.transaction(async (transaction) => { await user.demote(to, { transaction });
await user.demote(to, { transaction }); await Event.create(
await Event.create( {
{ name: "users.demote",
name: "users.demote", actorId,
actorId, userId: user.id,
userId: user.id, teamId: user.teamId,
teamId: user.teamId, data: {
data: { name: user.name,
name: user.name,
},
ip,
}, },
{ transaction } ip,
); },
await CleanupDemotedUserTask.schedule({ userId: user.id }); { transaction }
}); );
await CleanupDemotedUserTask.schedule({ userId: user.id });
} }

View File

@@ -1,12 +1,12 @@
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { User, Event, GroupUser } from "@server/models"; import { User, Event, GroupUser } from "@server/models";
import CleanupDemotedUserTask from "@server/queues/tasks/CleanupDemotedUserTask"; import CleanupDemotedUserTask from "@server/queues/tasks/CleanupDemotedUserTask";
import { sequelize } from "@server/storage/database";
import { ValidationError } from "../errors"; import { ValidationError } from "../errors";
type Props = { type Props = {
user: User; user: User;
actorId: string; actorId: string;
transaction?: Transaction;
ip: string; ip: string;
}; };
@@ -17,44 +17,43 @@ type Props = {
export default async function userSuspender({ export default async function userSuspender({
user, user,
actorId, actorId,
transaction,
ip, ip,
}: Props): Promise<void> { }: Props): Promise<void> {
if (user.id === actorId) { if (user.id === actorId) {
throw ValidationError("Unable to suspend the current user"); throw ValidationError("Unable to suspend the current user");
} }
await sequelize.transaction(async (transaction: Transaction) => { await user.update(
await user.update( {
{ suspendedById: actorId,
suspendedById: actorId, suspendedAt: new Date(),
suspendedAt: new Date(), },
}, {
{
transaction,
}
);
await GroupUser.destroy({
where: {
userId: user.id,
},
transaction, transaction,
}); }
await Event.create( );
{ await GroupUser.destroy({
name: "users.suspend", where: {
actorId, userId: user.id,
userId: user.id, },
teamId: user.teamId, transaction,
data: {
name: user.name,
},
ip,
},
{
transaction,
}
);
await CleanupDemotedUserTask.schedule({ userId: user.id });
}); });
await Event.create(
{
name: "users.suspend",
actorId,
userId: user.id,
teamId: user.teamId,
data: {
name: user.name,
},
ip,
},
{
transaction,
}
);
await CleanupDemotedUserTask.schedule({ userId: user.id });
} }

View File

@@ -1,11 +1,11 @@
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { User, Event } from "@server/models"; import { User, Event } from "@server/models";
import { sequelize } from "@server/storage/database";
import { ValidationError } from "../errors"; import { ValidationError } from "../errors";
type Props = { type Props = {
user: User; user: User;
actorId: string; actorId: string;
transaction?: Transaction;
ip: string; ip: string;
}; };
@@ -16,34 +16,33 @@ type Props = {
export default async function userUnsuspender({ export default async function userUnsuspender({
user, user,
actorId, actorId,
transaction,
ip, ip,
}: Props): Promise<void> { }: Props): Promise<void> {
if (user.id === actorId) { if (user.id === actorId) {
throw ValidationError("Unable to unsuspend the current user"); throw ValidationError("Unable to unsuspend the current user");
} }
await sequelize.transaction(async (transaction: Transaction) => { await user.update(
await user.update( {
{ suspendedById: null,
suspendedById: null, suspendedAt: null,
suspendedAt: null, },
{ transaction }
);
await Event.create(
{
name: "users.activate",
actorId,
userId: user.id,
teamId: user.teamId,
data: {
name: user.name,
}, },
{ transaction } ip,
); },
await Event.create( {
{ transaction,
name: "users.activate", }
actorId, );
userId: user.id,
teamId: user.teamId,
data: {
name: user.name,
},
ip,
},
{
transaction,
}
);
});
} }

View File

@@ -525,6 +525,7 @@ class User extends ParanoidModel {
}, },
}, },
limit: 1, limit: 1,
...options,
}); });
if (res.count >= 1) { if (res.count >= 1) {
@@ -563,11 +564,14 @@ class User extends ParanoidModel {
} }
}; };
promote = () => promote = (options?: SaveOptions<User>) =>
this.update({ this.update(
isAdmin: true, {
isViewer: false, isAdmin: true,
}); isViewer: false,
},
options
);
// hooks // hooks

View File

@@ -187,8 +187,8 @@ router.post(
router.post( router.post(
"users.update", "users.update",
auth(), auth(),
transaction(),
validate(T.UsersUpdateSchema), validate(T.UsersUpdateSchema),
transaction(),
async (ctx: APIContext<T.UsersUpdateReq>) => { async (ctx: APIContext<T.UsersUpdateReq>) => {
const { auth, transaction } = ctx.state; const { auth, transaction } = ctx.state;
const actor = auth.user; const actor = auth.user;
@@ -196,7 +196,11 @@ router.post(
let user: User | null = actor; let user: User | null = actor;
if (id) { if (id) {
user = await User.findByPk(id); user = await User.findByPk(id, {
rejectOnEmpty: true,
transaction,
lock: transaction.LOCK.UPDATE,
});
} }
authorize(actor, "update", user); authorize(actor, "update", user);
const includeDetails = can(actor, "readDetails", user); const includeDetails = can(actor, "readDetails", user);
@@ -240,24 +244,37 @@ router.post(
"users.promote", "users.promote",
auth(), auth(),
validate(T.UsersPromoteSchema), validate(T.UsersPromoteSchema),
transaction(),
async (ctx: APIContext<T.UsersPromoteReq>) => { async (ctx: APIContext<T.UsersPromoteReq>) => {
const { transaction } = ctx.state;
const userId = ctx.input.body.id; const userId = ctx.input.body.id;
const actor = ctx.state.auth.user; const actor = ctx.state.auth.user;
const teamId = actor.teamId; const teamId = actor.teamId;
const user = await User.findByPk(userId); const user = await User.findByPk(userId, {
rejectOnEmpty: true,
transaction,
lock: transaction.LOCK.UPDATE,
});
authorize(actor, "promote", user); authorize(actor, "promote", user);
await user.promote(); await user.promote({
await Event.create({ transaction,
name: "users.promote",
actorId: actor.id,
userId,
teamId,
data: {
name: user.name,
},
ip: ctx.request.ip,
}); });
await Event.create(
{
name: "users.promote",
actorId: actor.id,
userId,
teamId,
data: {
name: user.name,
},
ip: ctx.request.ip,
},
{
transaction,
}
);
const includeDetails = can(actor, "readDetails", user); const includeDetails = can(actor, "readDetails", user);
ctx.body = { ctx.body = {
@@ -273,20 +290,29 @@ router.post(
"users.demote", "users.demote",
auth(), auth(),
validate(T.UsersDemoteSchema), validate(T.UsersDemoteSchema),
transaction(),
async (ctx: APIContext<T.UsersDemoteReq>) => { async (ctx: APIContext<T.UsersDemoteReq>) => {
const userId = ctx.input.body.id; const { transaction } = ctx.state;
const to = ctx.input.body.to; const { to, id: userId } = ctx.input.body;
const actor = ctx.state.auth.user; const actor = ctx.state.auth.user;
const user = await User.findByPk(userId, { const user = await User.findByPk(userId, {
rejectOnEmpty: true, rejectOnEmpty: true,
transaction,
lock: transaction.LOCK.UPDATE,
}); });
authorize(actor, "demote", user); authorize(actor, "demote", user);
await Team.findByPk(user.teamId, {
transaction,
lock: transaction.LOCK.UPDATE,
});
await userDemoter({ await userDemoter({
to, to,
user, user,
actorId: actor.id, actorId: actor.id,
transaction,
ip: ctx.request.ip, ip: ctx.request.ip,
}); });
const includeDetails = can(actor, "readDetails", user); const includeDetails = can(actor, "readDetails", user);
@@ -304,11 +330,15 @@ router.post(
"users.suspend", "users.suspend",
auth(), auth(),
validate(T.UsersSuspendSchema), validate(T.UsersSuspendSchema),
transaction(),
async (ctx: APIContext<T.UsersSuspendReq>) => { async (ctx: APIContext<T.UsersSuspendReq>) => {
const { transaction } = ctx.state;
const userId = ctx.input.body.id; const userId = ctx.input.body.id;
const actor = ctx.state.auth.user; const actor = ctx.state.auth.user;
const user = await User.findByPk(userId, { const user = await User.findByPk(userId, {
rejectOnEmpty: true, rejectOnEmpty: true,
transaction,
lock: transaction.LOCK.UPDATE,
}); });
authorize(actor, "suspend", user); authorize(actor, "suspend", user);
@@ -316,6 +346,7 @@ router.post(
user, user,
actorId: actor.id, actorId: actor.id,
ip: ctx.request.ip, ip: ctx.request.ip,
transaction,
}); });
const includeDetails = can(actor, "readDetails", user); const includeDetails = can(actor, "readDetails", user);
@@ -332,17 +363,22 @@ router.post(
"users.activate", "users.activate",
auth(), auth(),
validate(T.UsersActivateSchema), validate(T.UsersActivateSchema),
transaction(),
async (ctx: APIContext<T.UsersActivateReq>) => { async (ctx: APIContext<T.UsersActivateReq>) => {
const { transaction } = ctx.state;
const userId = ctx.input.body.id; const userId = ctx.input.body.id;
const actor = ctx.state.auth.user; const actor = ctx.state.auth.user;
const user = await User.findByPk(userId, { const user = await User.findByPk(userId, {
rejectOnEmpty: true, rejectOnEmpty: true,
transaction,
lock: transaction.LOCK.UPDATE,
}); });
authorize(actor, "activate", user); authorize(actor, "activate", user);
await userUnsuspender({ await userUnsuspender({
user, user,
actorId: actor.id, actorId: actor.id,
transaction,
ip: ctx.request.ip, ip: ctx.request.ip,
}); });
const includeDetails = can(actor, "readDetails", user); const includeDetails = can(actor, "readDetails", user);
@@ -465,6 +501,8 @@ router.post(
if (id) { if (id) {
user = await User.findByPk(id, { user = await User.findByPk(id, {
rejectOnEmpty: true, rejectOnEmpty: true,
transaction,
lock: transaction.LOCK.UPDATE,
}); });
} else { } else {
user = actor; user = actor;