| const mongoose = require('mongoose'); |
| const { ObjectId } = require('mongodb'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { MongoMemoryServer } = require('mongodb-memory-server'); |
| const { |
| SystemRoles, |
| ResourceType, |
| AccessRoleIds, |
| PrincipalType, |
| PermissionBits, |
| } = require('librechat-data-provider'); |
|
|
| |
| jest.mock('../../config/connect', () => jest.fn().mockResolvedValue(true)); |
|
|
| const dbModels = require('~/db/models'); |
|
|
| |
| logger.silent = true; |
|
|
| let mongoServer; |
| let Prompt, PromptGroup, AclEntry, AccessRole, User, Group, Project; |
| let promptFns, permissionService; |
| let testUsers, testGroups, testRoles; |
|
|
| beforeAll(async () => { |
| |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| await mongoose.connect(mongoUri); |
|
|
| |
| Prompt = dbModels.Prompt; |
| PromptGroup = dbModels.PromptGroup; |
| AclEntry = dbModels.AclEntry; |
| AccessRole = dbModels.AccessRole; |
| User = dbModels.User; |
| Group = dbModels.Group; |
| Project = dbModels.Project; |
|
|
| promptFns = require('~/models/Prompt'); |
| permissionService = require('~/server/services/PermissionService'); |
|
|
| |
| await setupTestData(); |
| }); |
|
|
| afterAll(async () => { |
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| jest.clearAllMocks(); |
| }); |
|
|
| async function setupTestData() { |
| |
| testRoles = { |
| viewer: await AccessRole.create({ |
| accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, |
| name: 'Viewer', |
| description: 'Can view promptGroups', |
| resourceType: ResourceType.PROMPTGROUP, |
| permBits: PermissionBits.VIEW, |
| }), |
| editor: await AccessRole.create({ |
| accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, |
| name: 'Editor', |
| description: 'Can view and edit promptGroups', |
| resourceType: ResourceType.PROMPTGROUP, |
| permBits: PermissionBits.VIEW | PermissionBits.EDIT, |
| }), |
| owner: await AccessRole.create({ |
| accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, |
| name: 'Owner', |
| description: 'Full control over promptGroups', |
| resourceType: ResourceType.PROMPTGROUP, |
| permBits: |
| PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, |
| }), |
| }; |
|
|
| |
| testUsers = { |
| owner: await User.create({ |
| name: 'Prompt Owner', |
| email: 'owner@example.com', |
| role: SystemRoles.USER, |
| }), |
| editor: await User.create({ |
| name: 'Prompt Editor', |
| email: 'editor@example.com', |
| role: SystemRoles.USER, |
| }), |
| viewer: await User.create({ |
| name: 'Prompt Viewer', |
| email: 'viewer@example.com', |
| role: SystemRoles.USER, |
| }), |
| admin: await User.create({ |
| name: 'Admin User', |
| email: 'admin@example.com', |
| role: SystemRoles.ADMIN, |
| }), |
| noAccess: await User.create({ |
| name: 'No Access User', |
| email: 'noaccess@example.com', |
| role: SystemRoles.USER, |
| }), |
| }; |
|
|
| |
| testGroups = { |
| editors: await Group.create({ |
| name: 'Prompt Editors', |
| description: 'Group with editor access', |
| }), |
| viewers: await Group.create({ |
| name: 'Prompt Viewers', |
| description: 'Group with viewer access', |
| }), |
| }; |
|
|
| await Project.create({ |
| name: 'Global', |
| description: 'Global project', |
| promptGroupIds: [], |
| }); |
| } |
|
|
| describe('Prompt ACL Permissions', () => { |
| describe('Creating Prompts with Permissions', () => { |
| it('should grant owner permissions when creating a prompt', async () => { |
| |
| const testGroup = await PromptGroup.create({ |
| name: 'Test Group', |
| category: 'testing', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new mongoose.Types.ObjectId(), |
| }); |
|
|
| const promptData = { |
| prompt: { |
| prompt: 'Test prompt content', |
| name: 'Test Prompt', |
| type: 'text', |
| groupId: testGroup._id, |
| }, |
| author: testUsers.owner._id, |
| }; |
|
|
| await promptFns.savePrompt(promptData); |
|
|
| |
| await permissionService.grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: testUsers.owner._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testGroup._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, |
| grantedBy: testUsers.owner._id, |
| }); |
|
|
| |
| const aclEntry = await AclEntry.findOne({ |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testGroup._id, |
| principalType: PrincipalType.USER, |
| principalId: testUsers.owner._id, |
| }); |
|
|
| expect(aclEntry).toBeTruthy(); |
| expect(aclEntry.permBits).toBe(testRoles.owner.permBits); |
| }); |
| }); |
|
|
| describe('Accessing Prompts', () => { |
| let testPromptGroup; |
|
|
| beforeEach(async () => { |
| |
| testPromptGroup = await PromptGroup.create({ |
| name: 'Test Group', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new ObjectId(), |
| }); |
|
|
| |
| await Prompt.create({ |
| prompt: 'Test prompt for access control', |
| name: 'Access Test Prompt', |
| author: testUsers.owner._id, |
| groupId: testPromptGroup._id, |
| type: 'text', |
| }); |
|
|
| |
| await permissionService.grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: testUsers.owner._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, |
| grantedBy: testUsers.owner._id, |
| }); |
| }); |
|
|
| afterEach(async () => { |
| await Prompt.deleteMany({}); |
| await PromptGroup.deleteMany({}); |
| await AclEntry.deleteMany({}); |
| }); |
|
|
| it('owner should have full access to their prompt', async () => { |
| const hasAccess = await permissionService.checkPermission({ |
| userId: testUsers.owner._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.VIEW, |
| }); |
|
|
| expect(hasAccess).toBe(true); |
|
|
| const canEdit = await permissionService.checkPermission({ |
| userId: testUsers.owner._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.EDIT, |
| }); |
|
|
| expect(canEdit).toBe(true); |
| }); |
|
|
| it('user with viewer role should only have view access', async () => { |
| |
| await permissionService.grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: testUsers.viewer._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, |
| grantedBy: testUsers.owner._id, |
| }); |
|
|
| const canView = await permissionService.checkPermission({ |
| userId: testUsers.viewer._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.VIEW, |
| }); |
|
|
| const canEdit = await permissionService.checkPermission({ |
| userId: testUsers.viewer._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.EDIT, |
| }); |
|
|
| expect(canView).toBe(true); |
| expect(canEdit).toBe(false); |
| }); |
|
|
| it('user without permissions should have no access', async () => { |
| const hasAccess = await permissionService.checkPermission({ |
| userId: testUsers.noAccess._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.VIEW, |
| }); |
|
|
| expect(hasAccess).toBe(false); |
| }); |
|
|
| it('admin should have access regardless of permissions', async () => { |
| |
| |
| const hasAccess = await permissionService.checkPermission({ |
| userId: testUsers.admin._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.VIEW, |
| }); |
|
|
| |
| expect(hasAccess).toBe(false); |
|
|
| |
| |
| }); |
| }); |
|
|
| describe('Group-based Access', () => { |
| let testPromptGroup; |
|
|
| beforeEach(async () => { |
| |
| testPromptGroup = await PromptGroup.create({ |
| name: 'Group Access Test Group', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new ObjectId(), |
| }); |
|
|
| await Prompt.create({ |
| prompt: 'Group access test prompt', |
| name: 'Group Test', |
| author: testUsers.owner._id, |
| groupId: testPromptGroup._id, |
| type: 'text', |
| }); |
|
|
| |
| await User.findByIdAndUpdate(testUsers.editor._id, { |
| $push: { groups: testGroups.editors._id }, |
| }); |
|
|
| await User.findByIdAndUpdate(testUsers.viewer._id, { |
| $push: { groups: testGroups.viewers._id }, |
| }); |
| }); |
|
|
| afterEach(async () => { |
| await Prompt.deleteMany({}); |
| await AclEntry.deleteMany({}); |
| await User.updateMany({}, { $set: { groups: [] } }); |
| }); |
|
|
| it('group members should inherit group permissions', async () => { |
| |
| const testPromptGroup = await PromptGroup.create({ |
| name: 'Group Test Group', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new ObjectId(), |
| }); |
|
|
| const { addUserToGroup } = require('~/models'); |
| await addUserToGroup(testUsers.editor._id, testGroups.editors._id); |
|
|
| const prompt = await promptFns.savePrompt({ |
| author: testUsers.owner._id, |
| prompt: { |
| prompt: 'Group test prompt', |
| name: 'Group Test', |
| groupId: testPromptGroup._id, |
| type: 'text', |
| }, |
| }); |
|
|
| |
| if (!prompt || !prompt.prompt) { |
| throw new Error(`Failed to save prompt: ${prompt?.message || 'Unknown error'}`); |
| } |
|
|
| |
| await permissionService.grantPermission({ |
| principalType: PrincipalType.GROUP, |
| principalId: testGroups.editors._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, |
| grantedBy: testUsers.owner._id, |
| }); |
|
|
| |
| const hasAccess = await permissionService.checkPermission({ |
| userId: testUsers.editor._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.EDIT, |
| }); |
|
|
| expect(hasAccess).toBe(true); |
|
|
| |
| const nonMemberAccess = await permissionService.checkPermission({ |
| userId: testUsers.viewer._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| requiredPermission: PermissionBits.EDIT, |
| }); |
|
|
| expect(nonMemberAccess).toBe(false); |
| }); |
| }); |
|
|
| describe('Public Access', () => { |
| let publicPromptGroup, privatePromptGroup; |
|
|
| beforeEach(async () => { |
| |
| publicPromptGroup = await PromptGroup.create({ |
| name: 'Public Access Test Group', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new ObjectId(), |
| }); |
|
|
| privatePromptGroup = await PromptGroup.create({ |
| name: 'Private Access Test Group', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new ObjectId(), |
| }); |
|
|
| |
| await Prompt.create({ |
| prompt: 'Public prompt', |
| name: 'Public', |
| author: testUsers.owner._id, |
| groupId: publicPromptGroup._id, |
| type: 'text', |
| }); |
|
|
| await Prompt.create({ |
| prompt: 'Private prompt', |
| name: 'Private', |
| author: testUsers.owner._id, |
| groupId: privatePromptGroup._id, |
| type: 'text', |
| }); |
|
|
| |
| await permissionService.grantPermission({ |
| principalType: PrincipalType.PUBLIC, |
| principalId: null, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: publicPromptGroup._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, |
| grantedBy: testUsers.owner._id, |
| }); |
|
|
| |
| await permissionService.grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: testUsers.owner._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: privatePromptGroup._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, |
| grantedBy: testUsers.owner._id, |
| }); |
| }); |
|
|
| afterEach(async () => { |
| await Prompt.deleteMany({}); |
| await PromptGroup.deleteMany({}); |
| await AclEntry.deleteMany({}); |
| }); |
|
|
| it('public prompt should be accessible to any user', async () => { |
| const hasAccess = await permissionService.checkPermission({ |
| userId: testUsers.noAccess._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: publicPromptGroup._id, |
| requiredPermission: PermissionBits.VIEW, |
| includePublic: true, |
| }); |
|
|
| expect(hasAccess).toBe(true); |
| }); |
|
|
| it('private prompt should not be accessible to unauthorized users', async () => { |
| const hasAccess = await permissionService.checkPermission({ |
| userId: testUsers.noAccess._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: privatePromptGroup._id, |
| requiredPermission: PermissionBits.VIEW, |
| includePublic: true, |
| }); |
|
|
| expect(hasAccess).toBe(false); |
| }); |
| }); |
|
|
| describe('Prompt Deletion', () => { |
| let testPromptGroup; |
|
|
| it('should remove ACL entries when prompt is deleted', async () => { |
| testPromptGroup = await PromptGroup.create({ |
| name: 'Deletion Test Group', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new ObjectId(), |
| }); |
|
|
| const prompt = await promptFns.savePrompt({ |
| author: testUsers.owner._id, |
| prompt: { |
| prompt: 'To be deleted', |
| name: 'Delete Test', |
| groupId: testPromptGroup._id, |
| type: 'text', |
| }, |
| }); |
|
|
| |
| if (!prompt || !prompt.prompt) { |
| throw new Error(`Failed to save prompt: ${prompt?.message || 'Unknown error'}`); |
| } |
|
|
| const testPromptId = prompt.prompt._id; |
| const promptGroupId = testPromptGroup._id; |
|
|
| |
| await permissionService.grantPermission({ |
| principalType: PrincipalType.USER, |
| principalId: testUsers.owner._id, |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, |
| grantedBy: testUsers.owner._id, |
| }); |
|
|
| |
| const beforeDelete = await AclEntry.find({ |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| }); |
| expect(beforeDelete).toHaveLength(1); |
|
|
| |
| await promptFns.deletePrompt({ |
| promptId: testPromptId, |
| groupId: promptGroupId, |
| author: testUsers.owner._id, |
| role: SystemRoles.USER, |
| }); |
|
|
| |
| const aclEntries = await AclEntry.find({ |
| resourceType: ResourceType.PROMPTGROUP, |
| resourceId: testPromptGroup._id, |
| }); |
|
|
| expect(aclEntries).toHaveLength(0); |
| }); |
| }); |
|
|
| describe('Backwards Compatibility', () => { |
| it('should handle prompts without ACL entries gracefully', async () => { |
| |
| const promptGroup = await PromptGroup.create({ |
| name: 'Legacy Test Group', |
| author: testUsers.owner._id, |
| authorName: testUsers.owner.name, |
| productionId: new ObjectId(), |
| }); |
|
|
| |
| const legacyPrompt = await Prompt.create({ |
| prompt: 'Legacy prompt without ACL', |
| name: 'Legacy', |
| author: testUsers.owner._id, |
| groupId: promptGroup._id, |
| type: 'text', |
| }); |
|
|
| |
| const prompt = await promptFns.getPrompt({ _id: legacyPrompt._id }); |
| expect(prompt).toBeTruthy(); |
| expect(prompt._id.toString()).toBe(legacyPrompt._id.toString()); |
| }); |
| }); |
| }); |
|
|