LibreChat/api/server/routes/accessPermissions.test.js
Danny Avila f8774983a0
🪪 fix: Misleading MCP Server Lookup Method Name (#11315)
* 🔧 fix: MCP server ID resolver in access permissions (#11315)

- Replaced `findMCPServerById` with `findMCPServerByObjectId` in access permissions route and corresponding tests for improved clarity and consistency in resource identification.

* 🔧 refactor: Update MCP server resource access methods to use server name

- Replaced instances of `findMCPServerById` with `findMCPServerByServerName` across middleware, database, and test files for improved clarity and consistency in resource identification.
- Updated related comments and test cases to reflect the change in method usage.

* chore: Increase timeout for Redis update in GenerationJobManager integration tests

- Updated the timeout duration from 50ms to 200ms in the GenerationJobManager integration tests to ensure reliable verification of final event data in Redis after emitting the done event.
2026-01-12 21:04:25 -05:00

228 lines
7.3 KiB
JavaScript

const express = require('express');
const request = require('supertest');
const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { createMethods } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { ResourceType, PermissionBits } = require('librechat-data-provider');
const { createAgent } = require('~/models/Agent');
/**
* Mock the PermissionsController to isolate route testing
*/
jest.mock('~/server/controllers/PermissionsController', () => ({
getUserEffectivePermissions: jest.fn((req, res) => res.json({ permissions: [] })),
getAllEffectivePermissions: jest.fn((req, res) => res.json({ permissions: [] })),
updateResourcePermissions: jest.fn((req, res) => res.json({ success: true })),
getResourcePermissions: jest.fn((req, res) =>
res.json({
resourceType: req.params.resourceType,
resourceId: req.params.resourceId,
principals: [],
public: false,
}),
),
getResourceRoles: jest.fn((req, res) => res.json({ roles: [] })),
searchPrincipals: jest.fn((req, res) => res.json({ principals: [] })),
}));
jest.mock('~/server/middleware/checkPeoplePickerAccess', () => ({
checkPeoplePickerAccess: jest.fn((req, res, next) => next()),
}));
// Import actual middleware to get canAccessResource
const { canAccessResource } = require('~/server/middleware');
const { findMCPServerByObjectId } = require('~/models');
/**
* Security Tests for SBA-ADV-20251203-02
*
* These tests verify that users cannot query or modify agent permissions
* without proper SHARE permission.
*/
describe('Access Permissions Routes - Security Tests (SBA-ADV-20251203-02)', () => {
let app;
let mongoServer;
let authorId;
let attackerId;
let agentId;
let methods;
let User;
let modelsToCleanup = [];
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
// Initialize models
const { createModels } = require('@librechat/data-schemas');
const models = createModels(mongoose);
modelsToCleanup = Object.keys(models);
Object.assign(mongoose.models, models);
methods = createMethods(mongoose);
User = models.User;
await methods.seedDefaultRoles();
});
afterAll(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
for (const modelName of modelsToCleanup) {
delete mongoose.models[modelName];
}
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
// Clear all collections
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany({});
}
await methods.seedDefaultRoles();
// Create author (owner of the agent)
authorId = new mongoose.Types.ObjectId().toString();
await User.create({
_id: authorId,
name: 'Agent Owner',
email: 'owner@example.com',
username: 'owner@example.com',
provider: 'local',
});
// Create attacker (should not have access)
attackerId = new mongoose.Types.ObjectId().toString();
await User.create({
_id: attackerId,
name: 'Attacker',
email: 'attacker@example.com',
username: 'attacker@example.com',
provider: 'local',
});
// Create private agent owned by author
const customAgentId = `agent_${uuidv4().replace(/-/g, '').substring(0, 20)}`;
await createAgent({
id: customAgentId,
name: 'Private Agent',
provider: 'openai',
model: 'gpt-4',
author: authorId,
});
agentId = customAgentId;
// Create Express app with attacker as current user
app = express();
app.use(express.json());
// Mock authentication middleware - attacker is the current user
app.use((req, res, next) => {
req.user = { id: attackerId, role: 'USER' };
req.app = { locals: {} };
next();
});
// Middleware factory for permission access check (mirrors actual implementation)
const checkResourcePermissionAccess = (requiredPermission) => (req, res, next) => {
const { resourceType } = req.params;
let middleware;
if (resourceType === ResourceType.AGENT) {
middleware = canAccessResource({
resourceType: ResourceType.AGENT,
requiredPermission,
resourceIdParam: 'resourceId',
});
} else if (resourceType === ResourceType.PROMPTGROUP) {
middleware = canAccessResource({
resourceType: ResourceType.PROMPTGROUP,
requiredPermission,
resourceIdParam: 'resourceId',
});
} else if (resourceType === ResourceType.MCPSERVER) {
middleware = canAccessResource({
resourceType: ResourceType.MCPSERVER,
requiredPermission,
resourceIdParam: 'resourceId',
idResolver: findMCPServerByObjectId,
});
} else {
return res.status(400).json({
error: 'Bad Request',
message: `Unsupported resource type: ${resourceType}`,
});
}
middleware(req, res, next);
};
// GET route with access control (THE FIX)
app.get(
'/permissions/:resourceType/:resourceId',
checkResourcePermissionAccess(PermissionBits.SHARE),
(req, res) =>
res.json({
resourceType: req.params.resourceType,
resourceId: req.params.resourceId,
principals: [],
public: false,
}),
);
// PUT route with access control
app.put(
'/permissions/:resourceType/:resourceId',
checkResourcePermissionAccess(PermissionBits.SHARE),
(req, res) => res.json({ success: true }),
);
});
describe('GET /permissions/:resourceType/:resourceId', () => {
it('should deny permission query for user without access (main vulnerability test)', async () => {
/**
* SECURITY TEST: This is the core test for SBA-ADV-20251203-02
*
* Before the fix, any authenticated user could query permissions for
* any agent by just knowing the agent ID, exposing information about
* who has access to private agents.
*
* After the fix, users must have SHARE permission to view permissions.
*/
const response = await request(app)
.get(`/permissions/agent/${agentId}`)
.set('Content-Type', 'application/json');
// Should be denied - attacker has no permission on the agent
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
});
it('should return 400 for unsupported resource type', async () => {
const response = await request(app)
.get(`/permissions/unsupported/${agentId}`)
.set('Content-Type', 'application/json');
expect(response.status).toBe(400);
expect(response.body.message).toContain('Unsupported resource type');
});
});
describe('PUT /permissions/:resourceType/:resourceId', () => {
it('should deny permission update for user without access', async () => {
const response = await request(app)
.put(`/permissions/agent/${agentId}`)
.set('Content-Type', 'application/json')
.send({ principals: [] });
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
});
});
});