mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
fix(api): enforce single-role MFA, fix reindex and NCMEC scans (#2080)
This commit is contained in:
1 parent
8d21c97d08
commit
82941c08c9
4 files changed
+72
-16
No files matched your search
@@ -30,6 +30,7 @@ import {Config} from '../Config';
|
|||||||
import type {IChannelRepository} from '../channel/IChannelRepository';
|
import type {IChannelRepository} from '../channel/IChannelRepository';
|
||||||
import type {AttachmentUploadTraceRepository} from '../channel/repositories/message/AttachmentUploadTraceRepository';
|
import type {AttachmentUploadTraceRepository} from '../channel/repositories/message/AttachmentUploadTraceRepository';
|
||||||
import {
|
import {
|
||||||
|
collectMessageAttachments,
|
||||||
makeAttachmentCdnKey,
|
makeAttachmentCdnKey,
|
||||||
makeAttachmentCdnUrl,
|
makeAttachmentCdnUrl,
|
||||||
purgeMessageAttachments,
|
purgeMessageAttachments,
|
||||||
@@ -43,6 +44,9 @@ import type {IStorageService} from '../infrastructure/IStorageService';
|
|||||||
import type {KVAccountDeletionQueueService} from '../infrastructure/KVAccountDeletionQueueService';
|
import type {KVAccountDeletionQueueService} from '../infrastructure/KVAccountDeletionQueueService';
|
||||||
import type {UserCacheService} from '../infrastructure/UserCacheService';
|
import type {UserCacheService} from '../infrastructure/UserCacheService';
|
||||||
import {Logger} from '../Logger';
|
import {Logger} from '../Logger';
|
||||||
|
import type {Embed} from '../models/Embed';
|
||||||
|
import type {EmbedMedia} from '../models/EmbedMedia';
|
||||||
|
import type {Message} from '../models/Message';
|
||||||
import type {User} from '../models/User';
|
import type {User} from '../models/User';
|
||||||
import type {IARMessageContext, IARSubmission} from '../report/IReportRepository';
|
import type {IARMessageContext, IARSubmission} from '../report/IReportRepository';
|
||||||
import type {ReportRepository} from '../report/ReportRepository';
|
import type {ReportRepository} from '../report/ReportRepository';
|
||||||
@@ -471,18 +475,34 @@ export class NcmecSubmissionService {
|
|||||||
|
|
||||||
private async resolveAttachment(input: SubmitAttachmentToNcmecInput): Promise<ResolvedAttachment> {
|
private async resolveAttachment(input: SubmitAttachmentToNcmecInput): Promise<ResolvedAttachment> {
|
||||||
const liveMessage = await this.deps.channelRepository.getMessage(input.channelId, input.messageId);
|
const liveMessage = await this.deps.channelRepository.getMessage(input.channelId, input.messageId);
|
||||||
const liveAttachment = liveMessage?.attachments.find(
|
if (liveMessage?.authorId) {
|
||||||
(attachment) => attachment.id === input.attachmentId && attachment.filename === input.filename,
|
const liveAttachment = collectMessageAttachments(liveMessage).find(
|
||||||
);
|
(attachment) => attachment.id === input.attachmentId && attachment.filename === input.filename,
|
||||||
if (liveMessage?.authorId && liveAttachment) {
|
);
|
||||||
return this.buildAttachmentContext(input, {
|
if (liveAttachment) {
|
||||||
contentType: liveAttachment.contentType,
|
return this.buildAttachmentContext(input, {
|
||||||
reportedAt: snowflakeToDate(input.messageId),
|
contentType: liveAttachment.contentType,
|
||||||
userId: liveMessage.authorId,
|
reportedAt: snowflakeToDate(input.messageId),
|
||||||
sourceReportId: input.sourceReportId ?? null,
|
userId: liveMessage.authorId,
|
||||||
bucket: Config.s3.buckets.cdn,
|
sourceReportId: input.sourceReportId ?? null,
|
||||||
storageKey: makeAttachmentCdnKey(input.channelId, input.attachmentId, input.filename),
|
bucket: Config.s3.buckets.cdn,
|
||||||
});
|
storageKey: makeAttachmentCdnKey(input.channelId, input.attachmentId, input.filename),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const embedMedia = findEmbedReferencedAttachmentMedia(
|
||||||
|
liveMessage,
|
||||||
|
makeAttachmentCdnUrl(input.channelId, input.attachmentId, input.filename),
|
||||||
|
);
|
||||||
|
if (embedMedia) {
|
||||||
|
return this.buildAttachmentContext(input, {
|
||||||
|
contentType: embedMedia.contentType,
|
||||||
|
reportedAt: snowflakeToDate(input.messageId),
|
||||||
|
userId: liveMessage.authorId,
|
||||||
|
sourceReportId: input.sourceReportId ?? null,
|
||||||
|
bucket: Config.s3.buckets.cdn,
|
||||||
|
storageKey: makeAttachmentCdnKey(input.channelId, input.attachmentId, input.filename),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!input.sourceReportId) {
|
if (!input.sourceReportId) {
|
||||||
throw new UnknownMessageError();
|
throw new UnknownMessageError();
|
||||||
@@ -939,6 +959,30 @@ function buildAuditMetadata(args: {
|
|||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findEmbedReferencedAttachmentMedia(message: Message, targetCdnUrl: string): EmbedMedia | null {
|
||||||
|
const scan = (embeds: Array<Embed>): EmbedMedia | null => {
|
||||||
|
for (const embed of embeds) {
|
||||||
|
for (const media of [embed.image, embed.thumbnail, embed.video, embed.audio]) {
|
||||||
|
if (media?.url === targetCdnUrl) {
|
||||||
|
return media;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const direct = scan(message.embeds);
|
||||||
|
if (direct) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
for (const snapshot of message.messageSnapshots) {
|
||||||
|
const found = scan(snapshot.embeds);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function findAttachmentInReport(
|
function findAttachmentInReport(
|
||||||
report: IARSubmission,
|
report: IARSubmission,
|
||||||
channelId: ChannelID,
|
channelId: ChannelID,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class GuildMemberRoleService {
|
|||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const {userId, targetId, guildId, roleId} = params;
|
const {userId, targetId, guildId, roleId} = params;
|
||||||
const {guildData, canManageRoles} = await this.authService.getGuildAuthenticated({userId, guildId});
|
const {guildData, hasPermission, canManageRoles} = await this.authService.getGuildAuthenticated({userId, guildId});
|
||||||
const targetMember = await this.guildRepository.getMember(guildId, targetId);
|
const targetMember = await this.guildRepository.getMember(guildId, targetId);
|
||||||
if (!targetMember) throw new UnknownGuildMemberError();
|
if (!targetMember) throw new UnknownGuildMemberError();
|
||||||
await this.validationService.validateRoleAssignment({
|
await this.validationService.validateRoleAssignment({
|
||||||
@@ -51,6 +51,7 @@ export class GuildMemberRoleService {
|
|||||||
userId,
|
userId,
|
||||||
targetId,
|
targetId,
|
||||||
roleId,
|
roleId,
|
||||||
|
hasPermission,
|
||||||
canManageRoles,
|
canManageRoles,
|
||||||
});
|
});
|
||||||
if (targetMember.roleIds.has(roleId)) return;
|
if (targetMember.roleIds.has(roleId)) return;
|
||||||
@@ -75,7 +76,7 @@ export class GuildMemberRoleService {
|
|||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const {userId, targetId, guildId, roleId} = params;
|
const {userId, targetId, guildId, roleId} = params;
|
||||||
const {guildData, canManageRoles} = await this.authService.getGuildAuthenticated({userId, guildId});
|
const {guildData, hasPermission, canManageRoles} = await this.authService.getGuildAuthenticated({userId, guildId});
|
||||||
const targetMember = await this.guildRepository.getMember(guildId, targetId);
|
const targetMember = await this.guildRepository.getMember(guildId, targetId);
|
||||||
if (!targetMember) throw new UnknownGuildMemberError();
|
if (!targetMember) throw new UnknownGuildMemberError();
|
||||||
await this.validationService.validateRoleAssignment({
|
await this.validationService.validateRoleAssignment({
|
||||||
@@ -84,6 +85,7 @@ export class GuildMemberRoleService {
|
|||||||
userId,
|
userId,
|
||||||
targetId,
|
targetId,
|
||||||
roleId,
|
roleId,
|
||||||
|
hasPermission,
|
||||||
canManageRoles,
|
canManageRoles,
|
||||||
});
|
});
|
||||||
if (!targetMember.roleIds.has(roleId)) return;
|
if (!targetMember.roleIds.has(roleId)) return;
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ export class GuildMemberValidationService {
|
|||||||
userId: UserID;
|
userId: UserID;
|
||||||
targetId: UserID;
|
targetId: UserID;
|
||||||
roleId: RoleID;
|
roleId: RoleID;
|
||||||
|
hasPermission: (permission: bigint) => Promise<boolean>;
|
||||||
canManageRoles: (targetUserId: UserID, targetRoleId: RoleID) => Promise<boolean>;
|
canManageRoles: (targetUserId: UserID, targetRoleId: RoleID) => Promise<boolean>;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const {guildData, guildId, userId, targetId, roleId, canManageRoles} = params;
|
const {guildData, guildId, userId, targetId, roleId, hasPermission, canManageRoles} = params;
|
||||||
ensureNotEveryoneRole(roleId, guildId, 'role_id');
|
ensureNotEveryoneRole(roleId, guildId, 'role_id');
|
||||||
if (guildData && guildData.owner_id === userId.toString()) {
|
if (guildData && guildData.owner_id === userId.toString()) {
|
||||||
const role = await this.guildRepository.getRole(roleId, guildId);
|
const role = await this.guildRepository.getRole(roleId, guildId);
|
||||||
@@ -84,6 +85,9 @@ export class GuildMemberValidationService {
|
|||||||
throw new UnknownGuildRoleError();
|
throw new UnknownGuildRoleError();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!(await hasPermission(Permissions.MANAGE_ROLES))) {
|
||||||
|
throw new MissingPermissionsError();
|
||||||
|
}
|
||||||
if (!(await canManageRoles(targetId, roleId))) {
|
if (!(await canManageRoles(targetId, roleId))) {
|
||||||
throw new MissingPermissionsError();
|
throw new MissingPermissionsError();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from '../../SearchFactory';
|
} from '../../SearchFactory';
|
||||||
import type {IGuildMemberSearchService} from '../../search/IGuildMemberSearchService';
|
import type {IGuildMemberSearchService} from '../../search/IGuildMemberSearchService';
|
||||||
import type {IMessageSearchService} from '../../search/IMessageSearchService';
|
import type {IMessageSearchService} from '../../search/IMessageSearchService';
|
||||||
|
import {deleteChannelMessageSearchDocuments} from '../../search/MessageSearchIndexCleanup';
|
||||||
import {getWorkerDependencies} from '../WorkerContext';
|
import {getWorkerDependencies} from '../WorkerContext';
|
||||||
|
|
||||||
const INDEX_TYPES = [
|
const INDEX_TYPES = [
|
||||||
@@ -190,11 +191,16 @@ const refreshChannelMessages: IndexHandler = async (payload, helpers, kvClient,
|
|||||||
const {channelRepository} = getWorkerDependencies();
|
const {channelRepository} = getWorkerDependencies();
|
||||||
const guildId = createGuildID(BigInt(payload.guild_id!));
|
const guildId = createGuildID(BigInt(payload.guild_id!));
|
||||||
const searchService = requireSearchService<IMessageSearchService>(getMessageSearchService());
|
const searchService = requireSearchService<IMessageSearchService>(getMessageSearchService());
|
||||||
await searchService.deleteGuildMessages(guildId);
|
|
||||||
const channels = await channelRepository.listGuildChannels(guildId);
|
const channels = await channelRepository.listGuildChannels(guildId);
|
||||||
if (channels.length === 0) {
|
if (channels.length === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
for (const channel of channels) {
|
||||||
|
await deleteChannelMessageSearchDocuments(channel.id, {
|
||||||
|
searchService,
|
||||||
|
context: {source: 'bulk_reindex', guildId: guildId.toString()},
|
||||||
|
});
|
||||||
|
}
|
||||||
const completionKey = `bulk_reindex:${payload.job_id}:remaining`;
|
const completionKey = `bulk_reindex:${payload.job_id}:remaining`;
|
||||||
await kvClient.del(completionKey);
|
await kvClient.del(completionKey);
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
|
|||||||
Reference in new issue
Block a user