mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-09-01 19:41:32 +00:00
🧾 fix: Harden Historical File Authorization (#13918)
* fix: Harden historical file authorization * chore: Sort file authorization imports * fix: Preserve authorized historical artifact refs * chore: Format historical artifact hardening
This commit is contained in:
parent
606292c5c5
commit
1eb460eb03
11 changed files with 755 additions and 74 deletions
|
|
@ -809,10 +809,19 @@ describe('initializeAgent — attachment scoping', () => {
|
|||
db,
|
||||
);
|
||||
|
||||
expect(db.getToolFilesByIds).toHaveBeenCalledWith(
|
||||
[toolFile.file_id],
|
||||
new Set([EToolResources.file_search]),
|
||||
{ userId: 'user-1', tenantId: undefined },
|
||||
);
|
||||
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(1, [requestFile], undefined, {
|
||||
user: 'user-1',
|
||||
tenantId: undefined,
|
||||
});
|
||||
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(2, [toolFile], undefined, {
|
||||
user: 'user-1',
|
||||
tenantId: undefined,
|
||||
});
|
||||
expect(db.updateFilesUsage).toHaveBeenNthCalledWith(2, [toolFile]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -1992,13 +2001,17 @@ describe('initializeAgent — code-generated file thread filter (regression)', (
|
|||
);
|
||||
|
||||
expect(getCodeGeneratedFiles).toHaveBeenCalledTimes(1);
|
||||
expect(getCodeGeneratedFiles).toHaveBeenCalledWith('conv-1', [
|
||||
'file-pptx-skill',
|
||||
'file-output-csv',
|
||||
]);
|
||||
expect(getCodeGeneratedFiles).toHaveBeenCalledWith(
|
||||
'conv-1',
|
||||
['file-pptx-skill', 'file-output-csv'],
|
||||
{ userId: 'user-1', tenantId: undefined },
|
||||
);
|
||||
/* Both functions now share the same primary anchor — symmetric
|
||||
* design that closes the sibling-branch hole. */
|
||||
expect(getUserCodeFiles).toHaveBeenCalledWith(['file-pptx-skill', 'file-output-csv']);
|
||||
expect(getUserCodeFiles).toHaveBeenCalledWith(['file-pptx-skill', 'file-output-csv'], {
|
||||
userId: 'user-1',
|
||||
tenantId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('selects messages.attachments alongside messages.files (regression)', async () => {
|
||||
|
|
@ -2086,7 +2099,10 @@ describe('initializeAgent — code-generated file thread filter (regression)', (
|
|||
{ ...db, getMessages, getCodeGeneratedFiles, getUserCodeFiles },
|
||||
);
|
||||
|
||||
expect(getCodeGeneratedFiles).toHaveBeenCalledWith('conv-1', []);
|
||||
expect(getCodeGeneratedFiles).toHaveBeenCalledWith('conv-1', [], {
|
||||
userId: 'user-1',
|
||||
tenantId: undefined,
|
||||
});
|
||||
/* `getUserCodeFiles` is gated on a non-empty array at the call site,
|
||||
* so it shouldn't be invoked at all. `getCodeGeneratedFiles`'s own
|
||||
* empty-guard is exercised by data-schemas tests. */
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import type {
|
|||
TUser,
|
||||
} from 'librechat-data-provider';
|
||||
import type { GenericTool, LCToolRegistry, ToolMap, LCTool } from '@librechat/agents';
|
||||
import type { IMongoFile, FileOwnerScope } from '@librechat/data-schemas';
|
||||
import type { Response as ServerResponse } from 'express';
|
||||
import type { IMongoFile } from '@librechat/data-schemas';
|
||||
import type {
|
||||
ServerRequest,
|
||||
EndpointDbMethods,
|
||||
|
|
@ -411,22 +411,30 @@ export interface InitializeAgentDbMethods extends EndpointDbMethods {
|
|||
updateFilesUsage: (
|
||||
files: Array<{ file_id: string }>,
|
||||
fileIds?: string[],
|
||||
options?: { user?: string },
|
||||
options?: { user?: string; tenantId?: string | null },
|
||||
) => Promise<unknown[]>;
|
||||
/** Get files from database */
|
||||
getFiles: (filter: unknown, sort: unknown, select: unknown) => Promise<unknown[]>;
|
||||
/** Filter files by agent access permissions (ownership or agent attachment) */
|
||||
filterFilesByAgentAccess?: TFilterFilesByAgentAccess;
|
||||
/** Get tool files by IDs (user-uploaded files only, code files handled separately) */
|
||||
getToolFilesByIds: (fileIds: string[], toolSet: Set<EToolResources>) => Promise<unknown[]>;
|
||||
getToolFilesByIds: (
|
||||
fileIds: string[],
|
||||
toolSet: Set<EToolResources>,
|
||||
ownerScope?: FileOwnerScope,
|
||||
) => Promise<unknown[]>;
|
||||
/** Get conversation file IDs */
|
||||
getConvoFiles: (conversationId: string) => Promise<string[] | null>;
|
||||
/** Get code-generated files by conversation ID and the file_ids
|
||||
* referenced from messages in the current thread (collected via
|
||||
* `messages.files[].file_id` during thread walk). */
|
||||
getCodeGeneratedFiles?: (conversationId: string, threadFileIds?: string[]) => Promise<unknown[]>;
|
||||
getCodeGeneratedFiles?: (
|
||||
conversationId: string,
|
||||
threadFileIds?: string[],
|
||||
ownerScope?: FileOwnerScope,
|
||||
) => Promise<unknown[]>;
|
||||
/** Get user-uploaded execute_code files by file IDs (from message.files in thread) */
|
||||
getUserCodeFiles?: (fileIds: string[]) => Promise<unknown[]>;
|
||||
getUserCodeFiles?: (fileIds: string[], ownerScope: FileOwnerScope) => Promise<unknown[]>;
|
||||
/** Get messages for a conversation (supports select for field projection) */
|
||||
getMessages?: (
|
||||
filter: { conversationId: string },
|
||||
|
|
@ -563,6 +571,9 @@ export async function initializeAgent(
|
|||
isInitialAgent = false,
|
||||
} = params;
|
||||
const requestFileOwnerId = req.user?.id;
|
||||
const requestFileOwnerScope: FileOwnerScope | undefined = requestFileOwnerId
|
||||
? { userId: requestFileOwnerId, tenantId: req.user?.tenantId }
|
||||
: undefined;
|
||||
|
||||
if (!db) {
|
||||
throw new Error('initializeAgent requires db methods to be passed');
|
||||
|
|
@ -610,7 +621,13 @@ export async function initializeAgent(
|
|||
}
|
||||
}
|
||||
|
||||
const toolFiles = (await db.getToolFilesByIds(fileIds, toolResourceSet)) as IMongoFile[];
|
||||
const toolFiles = requestFileOwnerScope
|
||||
? ((await db.getToolFilesByIds(
|
||||
fileIds,
|
||||
toolResourceSet,
|
||||
requestFileOwnerScope,
|
||||
)) as IMongoFile[])
|
||||
: [];
|
||||
|
||||
/**
|
||||
* Retrieve execute_code files filtered to the current thread.
|
||||
|
|
@ -651,14 +668,25 @@ export async function initializeAgent(
|
|||
* which sibling first generated them — see `getCodeGeneratedFiles`
|
||||
* for the branched-conversation rationale. */
|
||||
if (db.getCodeGeneratedFiles) {
|
||||
codeGeneratedFiles = (await db.getCodeGeneratedFiles(
|
||||
conversationId,
|
||||
threadFileIds,
|
||||
)) as IMongoFile[];
|
||||
codeGeneratedFiles = requestFileOwnerScope
|
||||
? ((await db.getCodeGeneratedFiles(
|
||||
conversationId,
|
||||
threadFileIds,
|
||||
requestFileOwnerScope,
|
||||
)) as IMongoFile[])
|
||||
: [];
|
||||
}
|
||||
|
||||
if (db.getUserCodeFiles && threadFileIds && threadFileIds.length > 0) {
|
||||
userCodeFiles = (await db.getUserCodeFiles(threadFileIds)) as IMongoFile[];
|
||||
if (
|
||||
db.getUserCodeFiles &&
|
||||
requestFileOwnerScope &&
|
||||
threadFileIds &&
|
||||
threadFileIds.length > 0
|
||||
) {
|
||||
userCodeFiles = (await db.getUserCodeFiles(
|
||||
threadFileIds,
|
||||
requestFileOwnerScope,
|
||||
)) as IMongoFile[];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -668,21 +696,27 @@ export async function initializeAgent(
|
|||
requestFiles.length && requestFileOwnerId
|
||||
? ((await db.updateFilesUsage(requestFiles, undefined, {
|
||||
user: requestFileOwnerId,
|
||||
tenantId: req.user?.tenantId,
|
||||
})) as IMongoFile[])
|
||||
: [];
|
||||
const requestUsageFileIds = new Set(requestUsageFiles.map((file) => file.file_id));
|
||||
const trustedToolFiles = allToolFiles.filter(
|
||||
(file) => !requestUsageFileIds.has(file.file_id),
|
||||
);
|
||||
const toolUsageFiles = trustedToolFiles.length
|
||||
? ((await db.updateFilesUsage(trustedToolFiles)) as IMongoFile[])
|
||||
: [];
|
||||
let toolUsageFiles: IMongoFile[] = [];
|
||||
if (trustedToolFiles.length > 0 && requestFileOwnerId) {
|
||||
toolUsageFiles = (await db.updateFilesUsage(trustedToolFiles, undefined, {
|
||||
user: requestFileOwnerId,
|
||||
tenantId: req.user?.tenantId,
|
||||
})) as IMongoFile[];
|
||||
}
|
||||
currentFiles = requestUsageFiles.concat(toolUsageFiles);
|
||||
}
|
||||
} else if (requestFiles.length) {
|
||||
currentFiles = requestFileOwnerId
|
||||
? ((await db.updateFilesUsage(requestFiles, undefined, {
|
||||
user: requestFileOwnerId,
|
||||
tenantId: req.user?.tenantId,
|
||||
})) as IMongoFile[])
|
||||
: [];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue