🧠 refactor: Memoize MCP Permission Checks Per Request (#13419)

This commit is contained in:
Danny Avila 2026-05-30 18:32:06 -04:00 committed by GitHub
parent 100871c3ec
commit 479e9d59b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 314 additions and 25 deletions

View file

@ -6,7 +6,12 @@ import {
EndpointURLs,
} from 'librechat-data-provider';
import type { IRole, IUser } from '@librechat/data-schemas';
import { checkAccess, generateCheckAccess, skipAgentCheck } from './access';
import {
checkAccess,
checkAccessWithRequestCache,
generateCheckAccess,
skipAgentCheck,
} from './access';
// Mock logger
jest.mock('@librechat/data-schemas', () => ({
@ -269,6 +274,100 @@ describe('access middleware', () => {
});
});
describe('checkAccessWithRequestCache', () => {
const defaultParams = {
user: {
id: 'user123',
role: 'user',
email: 'test@example.com',
emailVerified: true,
provider: 'local',
} as IUser,
permissionType: PermissionTypes.MCP_SERVERS,
permissions: [Permissions.USE],
getRoleByName: jest.fn(),
};
const allowedRole = {
name: 'user',
permissions: {
[PermissionTypes.MCP_SERVERS]: {
[Permissions.USE]: true,
[Permissions.CREATE]: true,
},
},
} as unknown as IRole;
it('should memoize permission checks for the same request', async () => {
defaultParams.getRoleByName.mockResolvedValue(allowedRole);
const params = {
...defaultParams,
req: mockReq as Request,
};
await expect(checkAccessWithRequestCache(params)).resolves.toBe(true);
await expect(checkAccessWithRequestCache(params)).resolves.toBe(true);
expect(defaultParams.getRoleByName).toHaveBeenCalledTimes(1);
});
it('should share an in-flight permission check for the same request', async () => {
defaultParams.getRoleByName.mockResolvedValue(allowedRole);
const params = {
...defaultParams,
req: mockReq as Request,
};
await expect(
Promise.all([checkAccessWithRequestCache(params), checkAccessWithRequestCache(params)]),
).resolves.toEqual([true, true]);
expect(defaultParams.getRoleByName).toHaveBeenCalledTimes(1);
});
it('should isolate memoized checks between requests', async () => {
defaultParams.getRoleByName.mockResolvedValue(allowedRole);
await expect(
checkAccessWithRequestCache({
...defaultParams,
req: mockReq as Request,
}),
).resolves.toBe(true);
await expect(
checkAccessWithRequestCache({
...defaultParams,
req: { ...mockReq } as Request,
}),
).resolves.toBe(true);
expect(defaultParams.getRoleByName).toHaveBeenCalledTimes(2);
});
it('should use separate cache entries for different permissions', async () => {
defaultParams.getRoleByName.mockResolvedValue(allowedRole);
await expect(
checkAccessWithRequestCache({
...defaultParams,
req: mockReq as Request,
permissions: [Permissions.USE],
}),
).resolves.toBe(true);
await expect(
checkAccessWithRequestCache({
...defaultParams,
req: mockReq as Request,
permissions: [Permissions.CREATE],
}),
).resolves.toBe(true);
expect(defaultParams.getRoleByName).toHaveBeenCalledTimes(2);
});
});
describe('generateCheckAccess', () => {
it('should create middleware that allows access when user has permissions', async () => {
const mockRole = {

View file

@ -24,6 +24,54 @@ export function skipAgentCheck(req?: ServerRequest): boolean {
return !isAgentsEndpoint(req.body.endpoint);
}
export interface CheckAccessParams {
user: IUser;
req?: ServerRequest;
permissionType: PermissionTypes;
permissions: Permissions[];
bodyProps?: Record<Permissions, string[]>;
checkObject?: object;
/** If skipCheck function is provided and returns true, skip permission checking */
skipCheck?: (req?: ServerRequest) => boolean;
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
}
export type CheckAccessWithRequestCacheParams = Omit<
CheckAccessParams,
'bodyProps' | 'checkObject' | 'skipCheck'
>;
type RequestPermissionCache = Map<string, Promise<boolean>>;
const requestPermissionCacheKey = '__librechatRequestPermissionCache';
function getRequestPermissionCache(req?: ServerRequest): RequestPermissionCache | null {
if (!req) {
return null;
}
const reqWithCache = req as ServerRequest & {
[requestPermissionCacheKey]?: RequestPermissionCache;
};
if (!reqWithCache[requestPermissionCacheKey]) {
Object.defineProperty(reqWithCache, requestPermissionCacheKey, {
value: new Map<string, Promise<boolean>>(),
enumerable: false,
});
}
return reqWithCache[requestPermissionCacheKey] ?? null;
}
function getRequestPermissionCacheKey({
user,
permissionType,
permissions,
}: CheckAccessWithRequestCacheParams): string {
return [permissionType, [...permissions].sort().join(','), user.id, user.role].join(':');
}
/**
* Core function to check if a user has one or more required permissions
* @param user - The user object
@ -43,17 +91,7 @@ export const checkAccess = async ({
bodyProps = {} as Record<Permissions, string[]>,
checkObject = {},
skipCheck,
}: {
user: IUser;
req?: ServerRequest;
permissionType: PermissionTypes;
permissions: Permissions[];
bodyProps?: Record<Permissions, string[]>;
checkObject?: object;
/** If skipCheck function is provided and returns true, skip permission checking */
skipCheck?: (req?: ServerRequest) => boolean;
getRoleByName: (roleName: string, fieldsToSelect?: string | string[]) => Promise<IRole | null>;
}): Promise<boolean> => {
}: CheckAccessParams): Promise<boolean> => {
if (skipCheck && skipCheck(req)) {
return true;
}
@ -85,6 +123,35 @@ export const checkAccess = async ({
return false;
};
/**
* Checks simple role permissions using a per-request promise cache.
* Use this only for checks whose result is fully described by user, role, permission type, and permissions.
*/
export const checkAccessWithRequestCache = async (
params: CheckAccessWithRequestCacheParams,
): Promise<boolean> => {
if (!params.req || !params.user?.id || !params.user?.role) {
return await checkAccess(params);
}
const cache = getRequestPermissionCache(params.req);
if (!cache) {
return await checkAccess(params);
}
const cacheKey = getRequestPermissionCacheKey(params);
let cachedCheck = cache.get(cacheKey);
if (!cachedCheck) {
cachedCheck = checkAccess(params).catch((error) => {
cache.delete(cacheKey);
throw error;
});
cache.set(cacheKey, cachedCheck);
}
return await cachedCheck;
};
/**
* Middleware to check if a user has one or more required permissions, optionally based on `req.body` properties.
* @param permissionType - The type of permission to check.