mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids, rename enums to PascalCase refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids chore: move sharing related components to dedicated "Sharing" directory chore: remove PublicSharingToggle component and update index exports chore: move non-sidepanel agent components to `~/components/Agents` chore: move AgentCategoryDisplay component with tests chore: remove commented out code refactor: change PERMISSION_BITS from const to enum for better type safety refactor: reorganize imports in GenericGrantAccessDialog and update index exports for hooks refactor: update type definitions to use ACCESS_ROLE_IDS for improved type safety refactor: remove unused canAccessPromptResource middleware and related code refactor: remove unused prompt access roles from createAccessRoleMethods refactor: update resourceType in AclEntry type definition to remove unused 'prompt' value refactor: introduce ResourceType enum and update resourceType usage across data provider files for improved type safety refactor: update resourceType usage to ResourceType enum across sharing and permissions components for improved type safety refactor: standardize resourceType usage to ResourceType enum across agent and prompt models, permissions controller, and middleware for enhanced type safety refactor: update resourceType references from PROMPT_GROUP to PROMPTGROUP for consistency across models, middleware, and components refactor: standardize access role IDs and resource type usage across agent, file, and prompt models for improved type safety and consistency chore: add typedefs for TUpdateResourcePermissionsRequest and TUpdateResourcePermissionsResponse to enhance type definitions chore: move SearchPicker to PeoplePicker dir refactor: implement debouncing for query changes in SearchPicker for improved performance chore: fix typing, import order for agent admin settings fix: agent admin settings, prevent agent form submission refactor: rename `ACCESS_ROLE_IDS` to `AccessRoleIds` refactor: replace PermissionBits with PERMISSION_BITS refactor: replace PERMISSION_BITS with PermissionBits
385 lines
12 KiB
JavaScript
385 lines
12 KiB
JavaScript
const mongoose = require('mongoose');
|
|
const { ResourceType } = require('librechat-data-provider');
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
|
const { canAccessAgentResource } = require('./canAccessAgentResource');
|
|
const { User, Role, AclEntry } = require('~/db/models');
|
|
const { createAgent } = require('~/models/Agent');
|
|
|
|
describe('canAccessAgentResource middleware', () => {
|
|
let mongoServer;
|
|
let req, res, next;
|
|
let testUser;
|
|
|
|
beforeAll(async () => {
|
|
mongoServer = await MongoMemoryServer.create();
|
|
const mongoUri = mongoServer.getUri();
|
|
await mongoose.connect(mongoUri);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await mongoose.disconnect();
|
|
await mongoServer.stop();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await mongoose.connection.dropDatabase();
|
|
await Role.create({
|
|
name: 'test-role',
|
|
permissions: {
|
|
AGENTS: {
|
|
USE: true,
|
|
CREATE: true,
|
|
SHARED_GLOBAL: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Create a test user
|
|
testUser = await User.create({
|
|
email: 'test@example.com',
|
|
name: 'Test User',
|
|
username: 'testuser',
|
|
role: 'test-role',
|
|
});
|
|
|
|
req = {
|
|
user: { id: testUser._id.toString(), role: 'test-role' },
|
|
params: {},
|
|
};
|
|
res = {
|
|
status: jest.fn().mockReturnThis(),
|
|
json: jest.fn(),
|
|
};
|
|
next = jest.fn();
|
|
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
describe('middleware factory', () => {
|
|
test('should throw error if requiredPermission is not provided', () => {
|
|
expect(() => canAccessAgentResource({})).toThrow(
|
|
'canAccessAgentResource: requiredPermission is required and must be a number',
|
|
);
|
|
});
|
|
|
|
test('should throw error if requiredPermission is not a number', () => {
|
|
expect(() => canAccessAgentResource({ requiredPermission: '1' })).toThrow(
|
|
'canAccessAgentResource: requiredPermission is required and must be a number',
|
|
);
|
|
});
|
|
|
|
test('should create middleware with default resourceIdParam', () => {
|
|
const middleware = canAccessAgentResource({ requiredPermission: 1 });
|
|
expect(typeof middleware).toBe('function');
|
|
expect(middleware.length).toBe(3); // Express middleware signature
|
|
});
|
|
|
|
test('should create middleware with custom resourceIdParam', () => {
|
|
const middleware = canAccessAgentResource({
|
|
requiredPermission: 2,
|
|
resourceIdParam: 'agent_id',
|
|
});
|
|
expect(typeof middleware).toBe('function');
|
|
expect(middleware.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('permission checking with real agents', () => {
|
|
test('should allow access when user is the agent author', async () => {
|
|
// Create an agent owned by the test user
|
|
const agent = await createAgent({
|
|
id: `agent_${Date.now()}`,
|
|
name: 'Test Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: testUser._id,
|
|
});
|
|
|
|
// Create ACL entry for the author (owner permissions)
|
|
await AclEntry.create({
|
|
principalType: 'user',
|
|
principalId: testUser._id,
|
|
principalModel: 'User',
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
permBits: 15, // All permissions (1+2+4+8)
|
|
grantedBy: testUser._id,
|
|
});
|
|
|
|
req.params.id = agent.id;
|
|
|
|
const middleware = canAccessAgentResource({ requiredPermission: 1 }); // VIEW permission
|
|
await middleware(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should deny access when user is not the author and has no ACL entry', async () => {
|
|
// Create an agent owned by a different user
|
|
const otherUser = await User.create({
|
|
email: 'other@example.com',
|
|
name: 'Other User',
|
|
username: 'otheruser',
|
|
role: 'test-role',
|
|
});
|
|
|
|
const agent = await createAgent({
|
|
id: `agent_${Date.now()}`,
|
|
name: 'Other User Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: otherUser._id,
|
|
});
|
|
|
|
// Create ACL entry for the other user (owner)
|
|
await AclEntry.create({
|
|
principalType: 'user',
|
|
principalId: otherUser._id,
|
|
principalModel: 'User',
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
permBits: 15, // All permissions
|
|
grantedBy: otherUser._id,
|
|
});
|
|
|
|
req.params.id = agent.id;
|
|
|
|
const middleware = canAccessAgentResource({ requiredPermission: 1 }); // VIEW permission
|
|
await middleware(req, res, next);
|
|
|
|
expect(next).not.toHaveBeenCalled();
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Forbidden',
|
|
message: 'Insufficient permissions to access this agent',
|
|
});
|
|
});
|
|
|
|
test('should allow access when user has ACL entry with sufficient permissions', async () => {
|
|
// Create an agent owned by a different user
|
|
const otherUser = await User.create({
|
|
email: 'other2@example.com',
|
|
name: 'Other User 2',
|
|
username: 'otheruser2',
|
|
role: 'test-role',
|
|
});
|
|
|
|
const agent = await createAgent({
|
|
id: `agent_${Date.now()}`,
|
|
name: 'Shared Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: otherUser._id,
|
|
});
|
|
|
|
// Create ACL entry granting view permission to test user
|
|
await AclEntry.create({
|
|
principalType: 'user',
|
|
principalId: testUser._id,
|
|
principalModel: 'User',
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
permBits: 1, // VIEW permission
|
|
grantedBy: otherUser._id,
|
|
});
|
|
|
|
req.params.id = agent.id;
|
|
|
|
const middleware = canAccessAgentResource({ requiredPermission: 1 }); // VIEW permission
|
|
await middleware(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('should deny access when ACL permissions are insufficient', async () => {
|
|
// Create an agent owned by a different user
|
|
const otherUser = await User.create({
|
|
email: 'other3@example.com',
|
|
name: 'Other User 3',
|
|
username: 'otheruser3',
|
|
role: 'test-role',
|
|
});
|
|
|
|
const agent = await createAgent({
|
|
id: `agent_${Date.now()}`,
|
|
name: 'Limited Access Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: otherUser._id,
|
|
});
|
|
|
|
// Create ACL entry granting only view permission
|
|
await AclEntry.create({
|
|
principalType: 'user',
|
|
principalId: testUser._id,
|
|
principalModel: 'User',
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
permBits: 1, // VIEW permission only
|
|
grantedBy: otherUser._id,
|
|
});
|
|
|
|
req.params.id = agent.id;
|
|
|
|
const middleware = canAccessAgentResource({ requiredPermission: 2 }); // EDIT permission required
|
|
await middleware(req, res, next);
|
|
|
|
expect(next).not.toHaveBeenCalled();
|
|
expect(res.status).toHaveBeenCalledWith(403);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Forbidden',
|
|
message: 'Insufficient permissions to access this agent',
|
|
});
|
|
});
|
|
|
|
test('should handle non-existent agent', async () => {
|
|
req.params.id = 'agent_nonexistent';
|
|
|
|
const middleware = canAccessAgentResource({ requiredPermission: 1 });
|
|
await middleware(req, res, next);
|
|
|
|
expect(next).not.toHaveBeenCalled();
|
|
expect(res.status).toHaveBeenCalledWith(404);
|
|
expect(res.json).toHaveBeenCalledWith({
|
|
error: 'Not Found',
|
|
message: 'agent not found',
|
|
});
|
|
});
|
|
|
|
test('should use custom resourceIdParam', async () => {
|
|
const agent = await createAgent({
|
|
id: `agent_${Date.now()}`,
|
|
name: 'Custom Param Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: testUser._id,
|
|
});
|
|
|
|
// Create ACL entry for the author
|
|
await AclEntry.create({
|
|
principalType: 'user',
|
|
principalId: testUser._id,
|
|
principalModel: 'User',
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
permBits: 15, // All permissions
|
|
grantedBy: testUser._id,
|
|
});
|
|
|
|
req.params.agent_id = agent.id; // Using custom param name
|
|
|
|
const middleware = canAccessAgentResource({
|
|
requiredPermission: 1,
|
|
resourceIdParam: 'agent_id',
|
|
});
|
|
await middleware(req, res, next);
|
|
|
|
expect(next).toHaveBeenCalled();
|
|
expect(res.status).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('permission levels', () => {
|
|
let agent;
|
|
|
|
beforeEach(async () => {
|
|
agent = await createAgent({
|
|
id: `agent_${Date.now()}`,
|
|
name: 'Permission Test Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: testUser._id,
|
|
});
|
|
|
|
// Create ACL entry with all permissions for the owner
|
|
await AclEntry.create({
|
|
principalType: 'user',
|
|
principalId: testUser._id,
|
|
principalModel: 'User',
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
permBits: 15, // All permissions (1+2+4+8)
|
|
grantedBy: testUser._id,
|
|
});
|
|
|
|
req.params.id = agent.id;
|
|
});
|
|
|
|
test('should support view permission (1)', async () => {
|
|
const middleware = canAccessAgentResource({ requiredPermission: 1 });
|
|
await middleware(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should support edit permission (2)', async () => {
|
|
const middleware = canAccessAgentResource({ requiredPermission: 2 });
|
|
await middleware(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should support delete permission (4)', async () => {
|
|
const middleware = canAccessAgentResource({ requiredPermission: 4 });
|
|
await middleware(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should support share permission (8)', async () => {
|
|
const middleware = canAccessAgentResource({ requiredPermission: 8 });
|
|
await middleware(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
test('should support combined permissions', async () => {
|
|
const viewAndEdit = 1 | 2; // 3
|
|
const middleware = canAccessAgentResource({ requiredPermission: viewAndEdit });
|
|
await middleware(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('integration with agent operations', () => {
|
|
test('should work with agent CRUD operations', async () => {
|
|
const agentId = `agent_${Date.now()}`;
|
|
|
|
// Create agent
|
|
const agent = await createAgent({
|
|
id: agentId,
|
|
name: 'Integration Test Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: testUser._id,
|
|
description: 'Testing integration',
|
|
});
|
|
|
|
// Create ACL entry for the author
|
|
await AclEntry.create({
|
|
principalType: 'user',
|
|
principalId: testUser._id,
|
|
principalModel: 'User',
|
|
resourceType: ResourceType.AGENT,
|
|
resourceId: agent._id,
|
|
permBits: 15, // All permissions
|
|
grantedBy: testUser._id,
|
|
});
|
|
|
|
req.params.id = agentId;
|
|
|
|
// Test view access
|
|
const viewMiddleware = canAccessAgentResource({ requiredPermission: 1 });
|
|
await viewMiddleware(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
jest.clearAllMocks();
|
|
|
|
// Update the agent
|
|
const { updateAgent } = require('~/models/Agent');
|
|
await updateAgent({ id: agentId }, { description: 'Updated description' });
|
|
|
|
// Test edit access
|
|
const editMiddleware = canAccessAgentResource({ requiredPermission: 2 });
|
|
await editMiddleware(req, res, next);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|