fix(security): transcript access control (closes #555)

This commit is contained in:
Isaac 2024-09-06 03:59:24 +01:00
parent 2d2f350284
commit b8b5ac946a
No known key found for this signature in database
GPG Key ID: 17700D08381EA590
3 changed files with 69 additions and 30 deletions

View File

@ -18,12 +18,13 @@ module.exports = class TicketCompleter extends Autocompleter {
} }
async getOptions(value, { async getOptions(value, {
guildId, interaction,
open, open,
userId, userId,
}) { }) {
/** @type {import("client")} */ /** @type {import("client")} */
const client = this.client; const client = this.client;
const guildId = interaction.guild.id;
const cacheKey = [guildId, userId, open].join('/'); const cacheKey = [guildId, userId, open].join('/');
let tickets = await this.cache.get(cacheKey); let tickets = await this.cache.get(cacheKey);
@ -34,27 +35,22 @@ module.exports = class TicketCompleter extends Autocompleter {
where: { id: guildId }, where: { id: guildId },
}); });
tickets = await client.prisma.ticket.findMany({ tickets = await client.prisma.ticket.findMany({
include: { include: { category: true },
category: {
select: {
emoji: true,
name: true,
},
},
},
where: { where: {
createdById: userId, createdById: userId,
guildId, guildId,
open, open,
}, },
}); });
tickets = tickets.map(ticket => { tickets = tickets
const date = new Date(ticket.createdAt).toLocaleString([locale, 'en-GB'], { dateStyle: 'short' }); .filter(ticket => client.commands.commands.slash.get('transcript').shouldAllowAccess(interaction, ticket))
const topic = ticket.topic ? '- ' + decrypt(ticket.topic).replace(/\n/g, ' ').substring(0, 50) : ''; .map(ticket => {
const category = emoji.hasEmoji(ticket.category.emoji) ? emoji.get(ticket.category.emoji) + ' ' + ticket.category.name : ticket.category.name; const date = new Date(ticket.createdAt).toLocaleString([locale, 'en-GB'], { dateStyle: 'short' });
ticket._name = `${category} #${ticket.number} (${date}) ${topic}`; const topic = ticket.topic ? '- ' + decrypt(ticket.topic).replace(/\n/g, ' ').substring(0, 50) : '';
return ticket; const category = emoji.hasEmoji(ticket.category.emoji) ? emoji.get(ticket.category.emoji) + ' ' + ticket.category.name : ticket.category.name;
}); ticket._name = `${category} #${ticket.number} (${date}) ${topic}`;
return ticket;
});
this.cache.set(cacheKey, tickets, ms('1m')); this.cache.set(cacheKey, tickets, ms('1m'));
} }
@ -77,7 +73,7 @@ module.exports = class TicketCompleter extends Autocompleter {
const userId = otherMember || interaction.user.id; const userId = otherMember || interaction.user.id;
await interaction.respond( await interaction.respond(
await this.getOptions(value, { await this.getOptions(value, {
guildId: interaction.guild.id, interaction,
open: ['add', 'close', 'force-close', 'remove'].includes(command.name), // false for `new`, `transcript` etc open: ['add', 'close', 'force-close', 'remove'].includes(command.name), // false for `new`, `transcript` etc
userId, userId,
}), }),

View File

@ -1,5 +1,8 @@
const { SlashCommand } = require('@eartharoid/dbf'); const { SlashCommand } = require('@eartharoid/dbf');
const { ApplicationCommandOptionType } = require('discord.js'); const {
ApplicationCommandOptionType,
PermissionsBitField,
} = require('discord.js');
const { isStaff } = require('../../lib/users'); const { isStaff } = require('../../lib/users');
const ExtendedEmbedBuilder = require('../../lib/embed'); const ExtendedEmbedBuilder = require('../../lib/embed');
const Cryptr = require('cryptr'); const Cryptr = require('cryptr');
@ -60,12 +63,45 @@ module.exports = class TicketsSlashCommand extends SlashCommand {
} }
const fields = []; const fields = [];
let base_filter;
if (member.id === interaction.member.id) {
base_filter = {
createdById: member.id,
guildId: interaction.guild.id,
};
} else {
const { categories } = await client.prisma.guild.findUnique({
select: {
categories: {
select: {
id: true,
staffRoles: true,
},
},
},
where: { id: interaction.guild.id },
});
const allow_category_ids = (
(
client.supers.includes(interaction.member.id) ||
interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)
)
? categories
: categories.filter(c => c.staffRoles.some(id => interaction.member.roles.cache.has(id)))
)
.map(c => c.id);
base_filter = {
categoryId: { in: allow_category_ids },
createdById: member.id,
guildId: interaction.guild.id,
};
}
const open = await client.prisma.ticket.findMany({ const open = await client.prisma.ticket.findMany({
include: { category: true }, include: { category: true },
where: { where: {
createdById: member.id, ...base_filter,
guildId: interaction.guild.id,
open: true, open: true,
}, },
}); });
@ -75,8 +111,7 @@ module.exports = class TicketsSlashCommand extends SlashCommand {
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
take: 10, // max 10 rows take: 10, // max 10 rows
where: { where: {
createdById: member.id, ...base_filter,
guildId: interaction.guild.id,
open: false, open: false,
}, },
}); });
@ -84,8 +119,8 @@ module.exports = class TicketsSlashCommand extends SlashCommand {
if (open.length >= 1) { if (open.length >= 1) {
fields.push({ fields.push({
name: getMessage('commands.slash.tickets.response.fields.open.name'), name: getMessage('commands.slash.tickets.response.fields.open.name'),
value: open.map(ticket =>{ value: open.map(ticket => {
const topic = ticket.topic ? `- \`${decrypt(ticket.topic).replace(/\n/g, ' ').slice(0, 30) }\`` : ''; const topic = ticket.topic ? `- \`${decrypt(ticket.topic).replace(/\n/g, ' ').slice(0, 30)}\`` : '';
return `> <#${ticket.id}> ${topic}`; return `> <#${ticket.id}> ${topic}`;
}).join('\n'), }).join('\n'),
}); });

View File

@ -1,12 +1,14 @@
const { SlashCommand } = require('@eartharoid/dbf'); const { SlashCommand } = require('@eartharoid/dbf');
const { ApplicationCommandOptionType } = require('discord.js'); const {
ApplicationCommandOptionType,
PermissionsBitField,
} = require('discord.js');
const fs = require('fs'); const fs = require('fs');
const { join } = require('path'); const { join } = require('path');
const Mustache = require('mustache'); const Mustache = require('mustache');
const { AttachmentBuilder } = require('discord.js'); const { AttachmentBuilder } = require('discord.js');
const Cryptr = require('cryptr'); const Cryptr = require('cryptr');
const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY); const { decrypt } = new Cryptr(process.env.ENCRYPTION_KEY);
const { isStaff } = require('../../lib/users');
const ExtendedEmbedBuilder = require('../../lib/embed'); const ExtendedEmbedBuilder = require('../../lib/embed');
module.exports = class TranscriptSlashCommand extends SlashCommand { module.exports = class TranscriptSlashCommand extends SlashCommand {
@ -46,6 +48,15 @@ module.exports = class TranscriptSlashCommand extends SlashCommand {
); );
} }
shouldAllowAccess(interaction, ticket) {
if (interaction.guild.id !== ticket.guildId) return false;
if (ticket.createdById === interaction.member.id) return true;
if (interaction.client.supers.includes(interaction.member.id)) return true;
if (interaction.member.permissions.has(PermissionsBitField.Flags.ManageGuild)) return true;
if (interaction.member.roles.cache.filter(role => ticket.category.staffRoles.includes(role.id)).size > 0) return true;
return false;
}
async fillTemplate(ticket) { async fillTemplate(ticket) {
/** @type {import("client")} */ /** @type {import("client")} */
const client = this.client; const client = this.client;
@ -146,10 +157,7 @@ module.exports = class TranscriptSlashCommand extends SlashCommand {
if (!ticket) throw new Error(`Ticket ${ticketId} does not exist`); if (!ticket) throw new Error(`Ticket ${ticketId} does not exist`);
if ( if (!this.shouldAllowAccess(interaction, ticket)) {
ticket.createdById !== interaction.member.id &&
!(await isStaff(interaction.guild, interaction.member.id))
) {
const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } }); const settings = await client.prisma.guild.findUnique({ where: { id: interaction.guild.id } });
const getMessage = client.i18n.getLocale(settings.locale); const getMessage = client.i18n.getLocale(settings.locale);
return await interaction.editReply({ return await interaction.editReply({