| import mongoose from 'mongoose'; |
| import { |
| ResourceType, |
| PrincipalType, |
| PrincipalModel, |
| PermissionBits, |
| } from 'librechat-data-provider'; |
| import { MongoMemoryServer } from 'mongodb-memory-server'; |
| import type * as t from '~/types'; |
| import { createAclEntryMethods } from './aclEntry'; |
| import aclEntrySchema from '~/schema/aclEntry'; |
|
|
| let mongoServer: MongoMemoryServer; |
| let AclEntry: mongoose.Model<t.IAclEntry>; |
| let methods: ReturnType<typeof createAclEntryMethods>; |
|
|
| beforeAll(async () => { |
| mongoServer = await MongoMemoryServer.create(); |
| const mongoUri = mongoServer.getUri(); |
| AclEntry = mongoose.models.AclEntry || mongoose.model('AclEntry', aclEntrySchema); |
| methods = createAclEntryMethods(mongoose); |
| await mongoose.connect(mongoUri); |
| }); |
|
|
| afterAll(async () => { |
| await mongoose.disconnect(); |
| await mongoServer.stop(); |
| }); |
|
|
| beforeEach(async () => { |
| await mongoose.connection.dropDatabase(); |
| }); |
|
|
| describe('AclEntry Model Tests', () => { |
| |
| const userId = new mongoose.Types.ObjectId(); |
| const groupId = new mongoose.Types.ObjectId(); |
| const resourceId = new mongoose.Types.ObjectId(); |
| const grantedById = new mongoose.Types.ObjectId(); |
|
|
| describe('Permission Grant and Query', () => { |
| test('should grant permission to a user', async () => { |
| const entry = await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| expect(entry).toBeDefined(); |
| expect(entry?.principalType).toBe(PrincipalType.USER); |
| expect(entry?.principalId?.toString()).toBe(userId.toString()); |
| expect(entry?.principalModel).toBe(PrincipalModel.USER); |
| expect(entry?.resourceType).toBe(ResourceType.AGENT); |
| expect(entry?.resourceId.toString()).toBe(resourceId.toString()); |
| expect(entry?.permBits).toBe(PermissionBits.VIEW); |
| expect(entry?.grantedBy?.toString()).toBe(grantedById.toString()); |
| expect(entry?.grantedAt).toBeInstanceOf(Date); |
| }); |
|
|
| test('should grant permission to a group', async () => { |
| const entry = await methods.grantPermission( |
| PrincipalType.GROUP, |
| groupId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW | PermissionBits.EDIT, |
| grantedById, |
| ); |
|
|
| expect(entry).toBeDefined(); |
| expect(entry?.principalType).toBe(PrincipalType.GROUP); |
| expect(entry?.principalId?.toString()).toBe(groupId.toString()); |
| expect(entry?.principalModel).toBe(PrincipalModel.GROUP); |
| expect(entry?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); |
| }); |
|
|
| test('should grant public permission', async () => { |
| const entry = await methods.grantPermission( |
| PrincipalType.PUBLIC, |
| null, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| expect(entry).toBeDefined(); |
| expect(entry?.principalType).toBe(PrincipalType.PUBLIC); |
| expect(entry?.principalId).toBeUndefined(); |
| expect(entry?.principalModel).toBeUndefined(); |
| }); |
|
|
| test('should find entries by principal', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| 'project', |
| new mongoose.Types.ObjectId(), |
| PermissionBits.EDIT, |
| grantedById, |
| ); |
|
|
| |
| const entries = await methods.findEntriesByPrincipal(PrincipalType.USER, userId); |
| expect(entries).toHaveLength(2); |
|
|
| |
| const agentEntries = await methods.findEntriesByPrincipal( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| ); |
| expect(agentEntries).toHaveLength(1); |
| expect(agentEntries[0].resourceType).toBe(ResourceType.AGENT); |
| }); |
|
|
| test('should find entries by resource', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
| await methods.grantPermission( |
| PrincipalType.GROUP, |
| groupId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| grantedById, |
| ); |
| await methods.grantPermission( |
| PrincipalType.PUBLIC, |
| null, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| const entries = await methods.findEntriesByResource(ResourceType.AGENT, resourceId); |
| expect(entries).toHaveLength(3); |
| }); |
| }); |
|
|
| describe('Permission Checks', () => { |
| beforeEach(async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
| await methods.grantPermission( |
| PrincipalType.GROUP, |
| groupId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| grantedById, |
| ); |
|
|
| const otherResourceId = new mongoose.Types.ObjectId(); |
| await methods.grantPermission( |
| PrincipalType.PUBLIC, |
| null, |
| ResourceType.AGENT, |
| otherResourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
| }); |
|
|
| test('should find entries by principals and resource', async () => { |
| const principalsList = [ |
| { principalType: PrincipalType.USER, principalId: userId }, |
| { principalType: PrincipalType.GROUP, principalId: groupId }, |
| ]; |
|
|
| const entries = await methods.findEntriesByPrincipalsAndResource( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| ); |
| expect(entries).toHaveLength(2); |
| }); |
|
|
| test('should check if user has permission', async () => { |
| const principalsList = [{ principalType: PrincipalType.USER, principalId: userId }]; |
|
|
| |
| const hasViewPermission = await methods.hasPermission( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| ); |
| expect(hasViewPermission).toBe(true); |
|
|
| |
| const hasEditPermission = await methods.hasPermission( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| ); |
| expect(hasEditPermission).toBe(false); |
| }); |
|
|
| test('should check if group has permission', async () => { |
| const principalsList = [{ principalType: PrincipalType.GROUP, principalId: groupId }]; |
|
|
| |
| const hasEditPermission = await methods.hasPermission( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| ); |
| expect(hasEditPermission).toBe(true); |
| }); |
|
|
| test('should check permission for multiple principals', async () => { |
| const principalsList = [ |
| { principalType: PrincipalType.USER, principalId: userId }, |
| { principalType: PrincipalType.GROUP, principalId: groupId }, |
| ]; |
|
|
| |
| const hasViewPermission = await methods.hasPermission( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| ); |
| expect(hasViewPermission).toBe(true); |
|
|
| const hasEditPermission = await methods.hasPermission( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| ); |
| expect(hasEditPermission).toBe(true); |
|
|
| |
| const hasDeletePermission = await methods.hasPermission( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.DELETE, |
| ); |
| expect(hasDeletePermission).toBe(false); |
| }); |
|
|
| test('should get effective permissions', async () => { |
| const principalsList = [ |
| { principalType: PrincipalType.USER, principalId: userId }, |
| { principalType: PrincipalType.GROUP, principalId: groupId }, |
| ]; |
|
|
| const effective = await methods.getEffectivePermissions( |
| principalsList, |
| ResourceType.AGENT, |
| resourceId, |
| ); |
|
|
| |
| expect(effective).toBe(PermissionBits.VIEW | PermissionBits.EDIT); |
| }); |
| }); |
|
|
| describe('Permission Modification', () => { |
| test('should revoke permission', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const entriesBefore = await methods.findEntriesByPrincipal(PrincipalType.USER, userId); |
| expect(entriesBefore).toHaveLength(1); |
|
|
| |
| const result = await methods.revokePermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| ); |
| expect(result.deletedCount).toBe(1); |
|
|
| |
| const entriesAfter = await methods.findEntriesByPrincipal(PrincipalType.USER, userId); |
| expect(entriesAfter).toHaveLength(0); |
| }); |
|
|
| test('should modify permission bits - add permissions', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const updated = await methods.modifyPermissionBits( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| null, |
| ); |
|
|
| expect(updated).toBeDefined(); |
| expect(updated?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); |
| }); |
|
|
| test('should modify permission bits - remove permissions', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW | PermissionBits.EDIT, |
| grantedById, |
| ); |
|
|
| |
| const updated = await methods.modifyPermissionBits( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| null, |
| PermissionBits.EDIT, |
| ); |
|
|
| expect(updated).toBeDefined(); |
| expect(updated?.permBits).toBe(PermissionBits.VIEW); |
| }); |
|
|
| test('should modify permission bits - add and remove at once', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const updated = await methods.modifyPermissionBits( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| PermissionBits.VIEW, |
| ); |
|
|
| expect(updated).toBeDefined(); |
| expect(updated?.permBits).toBe(PermissionBits.EDIT); |
| }); |
| }); |
|
|
| describe('String vs ObjectId Edge Cases', () => { |
| test('should handle string userId in grantPermission', async () => { |
| const userIdString = userId.toString(); |
|
|
| const entry = await methods.grantPermission( |
| PrincipalType.USER, |
| userIdString, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| expect(entry).toBeDefined(); |
| expect(entry?.principalType).toBe(PrincipalType.USER); |
| |
| expect(entry?.principalId).toBeInstanceOf(mongoose.Types.ObjectId); |
| expect(entry?.principalId?.toString()).toBe(userIdString); |
| }); |
|
|
| test('should handle string groupId in grantPermission', async () => { |
| const groupIdString = groupId.toString(); |
|
|
| const entry = await methods.grantPermission( |
| PrincipalType.GROUP, |
| groupIdString, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| expect(entry).toBeDefined(); |
| expect(entry?.principalType).toBe(PrincipalType.GROUP); |
| |
| expect(entry?.principalId).toBeInstanceOf(mongoose.Types.ObjectId); |
| expect(entry?.principalId?.toString()).toBe(groupIdString); |
| }); |
|
|
| test('should handle string roleId in grantPermission for ROLE type', async () => { |
| const roleString = 'admin'; |
|
|
| const entry = await methods.grantPermission( |
| PrincipalType.ROLE, |
| roleString, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| expect(entry).toBeDefined(); |
| expect(entry?.principalType).toBe(PrincipalType.ROLE); |
| |
| expect(typeof entry?.principalId).toBe('string'); |
| expect(entry?.principalId).toBe(roleString); |
| expect(entry?.principalModel).toBe(PrincipalModel.ROLE); |
| }); |
|
|
| test('should handle string principalId in revokePermission', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const result = await methods.revokePermission( |
| PrincipalType.USER, |
| userId.toString(), |
| ResourceType.AGENT, |
| resourceId, |
| ); |
|
|
| expect(result.deletedCount).toBe(1); |
|
|
| |
| const entries = await methods.findEntriesByPrincipal(PrincipalType.USER, userId); |
| expect(entries).toHaveLength(0); |
| }); |
|
|
| test('should handle string principalId in modifyPermissionBits', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const updated = await methods.modifyPermissionBits( |
| PrincipalType.USER, |
| userId.toString(), |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.EDIT, |
| null, |
| ); |
|
|
| expect(updated).toBeDefined(); |
| expect(updated?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); |
| }); |
|
|
| test('should handle mixed string and ObjectId in hasPermission', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId.toString(), |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const hasPermWithObjectId = await methods.hasPermission( |
| [{ principalType: PrincipalType.USER, principalId: userId }], |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| ); |
| expect(hasPermWithObjectId).toBe(true); |
|
|
| |
| const hasPermWithString = await methods.hasPermission( |
| [{ principalType: PrincipalType.USER, principalId: userId.toString() }], |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| ); |
| expect(hasPermWithString).toBe(false); |
|
|
| |
| const hasPermWithConvertedId = await methods.hasPermission( |
| [ |
| { |
| principalType: PrincipalType.USER, |
| principalId: new mongoose.Types.ObjectId(userId.toString()), |
| }, |
| ], |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| ); |
| expect(hasPermWithConvertedId).toBe(true); |
| }); |
|
|
| test('should update existing permission when granting with string ID', async () => { |
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const updated = await methods.grantPermission( |
| PrincipalType.USER, |
| userId.toString(), |
| ResourceType.AGENT, |
| resourceId, |
| PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE, |
| grantedById, |
| ); |
|
|
| expect(updated).toBeDefined(); |
| expect(updated?.permBits).toBe( |
| PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE, |
| ); |
|
|
| |
| const entries = await methods.findEntriesByPrincipal(PrincipalType.USER, userId); |
| expect(entries).toHaveLength(1); |
| }); |
| }); |
|
|
| describe('Resource Access Queries', () => { |
| test('should find accessible resources', async () => { |
| |
| const resourceId1 = new mongoose.Types.ObjectId(); |
| const resourceId2 = new mongoose.Types.ObjectId(); |
| const resourceId3 = new mongoose.Types.ObjectId(); |
|
|
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId1, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| await methods.grantPermission( |
| PrincipalType.USER, |
| userId, |
| ResourceType.AGENT, |
| resourceId2, |
| PermissionBits.VIEW | PermissionBits.EDIT, |
| grantedById, |
| ); |
|
|
| |
| await methods.grantPermission( |
| PrincipalType.GROUP, |
| groupId, |
| ResourceType.AGENT, |
| resourceId3, |
| PermissionBits.VIEW, |
| grantedById, |
| ); |
|
|
| |
| const userViewableResources = await methods.findAccessibleResources( |
| [{ principalType: PrincipalType.USER, principalId: userId }], |
| ResourceType.AGENT, |
| PermissionBits.VIEW, |
| ); |
|
|
| expect(userViewableResources).toHaveLength(2); |
| expect(userViewableResources.map((r) => r.toString()).sort()).toEqual( |
| [resourceId1.toString(), resourceId2.toString()].sort(), |
| ); |
|
|
| |
| const allViewableResources = await methods.findAccessibleResources( |
| [ |
| { principalType: PrincipalType.USER, principalId: userId }, |
| { principalType: PrincipalType.GROUP, principalId: groupId }, |
| ], |
| ResourceType.AGENT, |
| PermissionBits.VIEW, |
| ); |
|
|
| expect(allViewableResources).toHaveLength(3); |
|
|
| |
| const editableResources = await methods.findAccessibleResources( |
| [{ principalType: PrincipalType.USER, principalId: userId }], |
| ResourceType.AGENT, |
| PermissionBits.EDIT, |
| ); |
|
|
| expect(editableResources).toHaveLength(1); |
| expect(editableResources[0].toString()).toBe(resourceId2.toString()); |
| }); |
|
|
| test('should handle inherited permissions', async () => { |
| const projectId = new mongoose.Types.ObjectId(); |
| const childResourceId = new mongoose.Types.ObjectId(); |
|
|
| |
| await AclEntry.create({ |
| principalType: PrincipalType.USER, |
| principalId: userId, |
| principalModel: PrincipalModel.USER, |
| resourceType: ResourceType.AGENT, |
| resourceId: childResourceId, |
| permBits: PermissionBits.VIEW, |
| grantedBy: grantedById, |
| inheritedFrom: projectId, |
| }); |
|
|
| |
| const effective = await methods.getEffectivePermissions( |
| [{ principalType: PrincipalType.USER, principalId: userId }], |
| ResourceType.AGENT, |
| childResourceId, |
| ); |
|
|
| |
| expect(effective).toBe(PermissionBits.VIEW); |
| }); |
| }); |
| }); |
|
|