fix(housekeeping): close final authorization gaps

This commit is contained in:
Simo committed 2026-08-30 21:01:32 +02:00
1 parent 2b8f73a91d
commit 222535e116
9 files changed
+219 -44

No files matched your search

@@ -149,6 +149,8 @@ function operationCapability(operation: PeopleMutationOperation) {
if (
operation === "user.ban" ||
operation === "user.unban" ||
operation === "users.bulk-ban" ||
operation === "users.bulk-unban" ||
operation === "ban.create" ||
operation === "ban.lift" ||
operation === "help-ticket.unban"
@@ -400,7 +402,7 @@ async function loadTarget(
if (
guardHierarchy &&
target.rank >= context.capability.actor.rank &&
context.capability.actor.rank < 7
!context.capability.isSuperAdmin
) {
throw new PeopleMutationFailure(
"FORBIDDEN",
@@ -410,6 +412,43 @@ async function loadTarget(
return target;
}
async function assertBulkTargetHierarchy(
userIds: readonly number[],
context: PeopleMutationContext,
): Promise<void> {
const uniqueIds = [...new Set(userIds)];
const targets = await db
.select({ id: User.id, rank: User.rank })
.from(User)
.where(inArray(User.id, uniqueIds));
if (targets.length !== uniqueIds.length) {
throw new PeopleMutationFailure(
"NOT_FOUND",
"errors.housekeeping.notFound",
);
}
if (
!context.capability.isSuperAdmin &&
targets.some((target) => target.rank >= context.capability.actor.rank)
) {
throw new PeopleMutationFailure(
"FORBIDDEN",
"errors.housekeeping.forbidden",
);
}
}
async function assertConfiguredRankExists(rank: number): Promise<void> {
const [rows] = (await db.execute(
sql`SELECT id FROM permission_ranks WHERE id = ${rank} LIMIT 1`,
)) as unknown as [unknown[], unknown];
if (!Array.isArray(rows) || rows.length === 0) {
throw new PeopleMutationFailure("VALIDATION", "errors.validation.invalid", {
rank: ["errors.validation.invalid"],
});
}
}
function targetSnapshot(target: TargetUser) {
return {
id: target.id,
@@ -592,15 +631,18 @@ async function executeUserMutation(
if (operation === "user.update") {
const fields = record(data.fields);
const nextRank = fields.rank;
if (
nextRank !== undefined &&
positiveInteger(nextRank) >= context.capability.actor.rank &&
context.capability.actor.rank < 7
) {
throw new PeopleMutationFailure(
"FORBIDDEN",
"errors.housekeeping.forbidden",
);
if (nextRank !== undefined) {
const rank = positiveInteger(nextRank);
if (
rank >= context.capability.actor.rank &&
!context.capability.isSuperAdmin
) {
throw new PeopleMutationFailure(
"FORBIDDEN",
"errors.housekeeping.forbidden",
);
}
await assertConfiguredRankExists(rank);
}
const userPatch = Object.fromEntries(
["username", "mail", "rank", "motto", "credits", "pixels"].flatMap(
@@ -1072,6 +1114,7 @@ async function executeBulkMutation(
): Promise<PeopleMutationSnapshot> {
const data = record(input);
const ids = userIds(data.userIds, context.legacy);
await assertBulkTargetHierarchy(ids, context);
const failures: Array<{ userId: number; reason: string }> = [];
const externalSyncFailures: Array<{ userId: number; reason: string }> = [];
const externalSyncFailureReason =