LibreChat/packages/api/src/acl/accessControlService.spec.ts
Atef Bellaaj 99f8bd2ce6
🏗️ feat: Dynamic MCP Server Infrastructure with Access Control (#10787)
* Feature: Dynamic MCP Server with Full UI Management

* 🚦 feat: Add MCP Connection Status icons to MCPBuilder panel (#10805)

* feature: Add MCP server connection status icons to MCPBuilder panel

* refactor: Simplify MCPConfigDialog rendering in MCPBuilderPanel

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Danny Avila <danny@librechat.ai>

* fix: address code review feedback for MCP server management

- Fix OAuth secret preservation to avoid mutating input parameter
  by creating a merged config copy in ServerConfigsDB.update()

- Improve error handling in getResourcePermissionsMap to propagate
  critical errors instead of silently returning empty Map

- Extract duplicated MCP server filter logic by exposing selectableServers
  from useMCPServerManager hook and using it in MCPSelect component

* test: Update PermissionService tests to throw errors on invalid resource types

- Changed the test for handling invalid resource types to ensure it throws an error instead of returning an empty permissions map.
- Updated the expectation to check for the specific error message when an invalid resource type is provided.

* feat: Implement retry logic for MCP server creation to handle race conditions

- Enhanced the createMCPServer method to include retry logic with exponential backoff for handling duplicate key errors during concurrent server creation.
- Updated tests to verify that all concurrent requests succeed and that unique server names are generated.
- Added a helper function to identify MongoDB duplicate key errors, improving error handling during server creation.

* refactor: StatusIcon to use CircleCheck for connected status

- Replaced the PlugZap icon with CircleCheck in the ConnectedStatusIcon component to better represent the connected state.
- Ensured consistent icon usage across the component for improved visual clarity.

* test: Update AccessControlService tests to throw errors on invalid resource types

- Modified the test for invalid resource types to ensure it throws an error with a specific message instead of returning an empty permissions map.
- This change enhances error handling and improves test coverage for the AccessControlService.

* fix: Update error message for missing server name in MCP server retrieval

- Changed the error message returned when the server name is not provided from 'MCP ID is required' to 'Server name is required' for better clarity and accuracy in the API response.

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-11 16:38:37 -05:00

1084 lines
36 KiB
TypeScript

import mongoose, { Types, Model } from 'mongoose';
import { createModels, createMethods, RoleBits } from '@librechat/data-schemas';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { ResourceType, AccessRoleIds, PrincipalType } from 'librechat-data-provider';
import { AccessControlService } from './accessControlService';
// Mock the logger
jest.mock('@librechat/data-schemas', () => ({
...jest.requireActual('@librechat/data-schemas'),
logger: {
error: jest.fn(),
warn: jest.fn(),
debug: jest.fn(),
info: jest.fn(),
},
}));
let mongoServer: MongoMemoryServer;
let AclEntry: Model<unknown>;
let service: AccessControlService;
let dbMethods: ReturnType<typeof createMethods>;
// Mock getUserPrincipals to control test scenarios
const mockGetUserPrincipals = jest.fn();
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
await mongoose.connect(mongoUri);
// Initialize all models
createModels(mongoose);
AclEntry = mongoose.models.AclEntry;
// Create methods and seed default roles
dbMethods = createMethods(mongoose);
await dbMethods.seedDefaultRoles();
// Create service instance
service = new AccessControlService(mongoose);
// Mock getUserPrincipals in the dbMethods
const originalMethods = service['_dbMethods'];
service['_dbMethods'] = {
...originalMethods,
getUserPrincipals: mockGetUserPrincipals,
};
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
// Clear test data but keep seeded roles
await AclEntry.deleteMany({});
mockGetUserPrincipals.mockReset();
});
describe('AccessControlService', () => {
// Common test data
const userId = new Types.ObjectId();
const groupId = new Types.ObjectId();
const resourceId = new Types.ObjectId();
const grantedById = new Types.ObjectId();
describe('grantPermission', () => {
describe('validation', () => {
test('should throw error for invalid principal type', async () => {
await expect(
service.grantPermission({
principalType: 'invalid' as PrincipalType,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Invalid principal type: invalid');
});
test('should throw error for missing principalId with user type', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.USER,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Principal ID is required for user, group, and role principals');
});
test('should throw error for missing principalId with group type', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.GROUP,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Principal ID is required for user, group, and role principals');
});
test('should throw error for missing principalId with role type', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.ROLE,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Principal ID is required for user, group, and role principals');
});
test('should throw error for invalid role ID (empty string)', async () => {
// Empty string is falsy, so it triggers the "principalId required" check first
await expect(
service.grantPermission({
principalType: PrincipalType.ROLE,
principalId: '',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Principal ID is required for user, group, and role principals');
});
test('should throw error for invalid role ID (whitespace only)', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.ROLE,
principalId: ' ',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Invalid role ID:');
});
test('should throw error for invalid user principal ID (non-ObjectId)', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.USER,
principalId: 'invalid-id',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Invalid principal ID: invalid-id');
});
test('should throw error for invalid resource ID', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: 'invalid-id',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Invalid resource ID: invalid-id');
});
test('should throw error for invalid resource type', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: 'invalidType' as ResourceType,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Invalid resourceType: invalidType');
});
});
describe('role lookup', () => {
test('should throw error for non-existent role', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'non_existent_role' as AccessRoleIds,
grantedBy: grantedById,
}),
).rejects.toThrow('Role non_existent_role not found');
});
test('should throw error for role-resource type mismatch', async () => {
await expect(
service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, // PromptGroup role for agent resource
grantedBy: grantedById,
}),
).rejects.toThrow('Role promptGroup_viewer is for promptGroup resources, not agent');
});
});
describe('successful grant', () => {
test('should grant permission to a user with a role', async () => {
const entry = await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
expect(entry).toBeDefined();
expect(entry!.principalType).toBe(PrincipalType.USER);
expect(entry!.principalId!.toString()).toBe(userId.toString());
expect(entry!.resourceType).toBe(ResourceType.AGENT);
expect(entry!.resourceId.toString()).toBe(resourceId.toString());
expect(entry!.permBits).toBe(RoleBits.VIEWER);
});
test('should grant permission to a group with a role', async () => {
const entry = await service.grantPermission({
principalType: PrincipalType.GROUP,
principalId: groupId,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
expect(entry).toBeDefined();
expect(entry!.principalType).toBe(PrincipalType.GROUP);
expect(entry!.principalId!.toString()).toBe(groupId.toString());
expect(entry!.permBits).toBe(RoleBits.EDITOR);
});
test('should grant public permission with a role', async () => {
const entry = await service.grantPermission({
principalType: PrincipalType.PUBLIC,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
expect(entry).toBeDefined();
expect(entry!.principalType).toBe(PrincipalType.PUBLIC);
expect(entry!.principalId).toBeUndefined();
expect(entry!.permBits).toBe(RoleBits.VIEWER);
});
test('should grant permission to a role principal', async () => {
const entry = await service.grantPermission({
principalType: PrincipalType.ROLE,
principalId: 'admin',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
expect(entry).toBeDefined();
expect(entry!.principalType).toBe(PrincipalType.ROLE);
expect(entry!.principalId).toBe('admin');
expect(entry!.permBits).toBe(RoleBits.EDITOR);
});
test('should update existing permission when granting to same principal and resource', async () => {
// First grant with viewer role
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
// Then update to editor role
const updated = await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
expect(updated!.permBits).toBe(RoleBits.EDITOR);
// Verify there's only one entry
const entries = await AclEntry.find({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId,
});
expect(entries).toHaveLength(1);
});
});
});
describe('findAccessibleResources', () => {
const resource1 = new Types.ObjectId();
const resource2 = new Types.ObjectId();
const resource3 = new Types.ObjectId();
beforeEach(async () => {
// User can view resource 1
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: resource1,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
// User can edit resource 2
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: resource2,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
// Group can view resource 3
await service.grantPermission({
principalType: PrincipalType.GROUP,
principalId: groupId,
resourceType: ResourceType.AGENT,
resourceId: resource3,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
});
describe('validation errors', () => {
test('should throw error when requiredPermissions is not a positive number', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
]);
await expect(
service.findAccessibleResources({
userId,
resourceType: ResourceType.AGENT,
requiredPermissions: 0,
}),
).rejects.toThrow('requiredPermissions must be a positive number');
});
test('should throw error when requiredPermissions is negative', async () => {
await expect(
service.findAccessibleResources({
userId,
resourceType: ResourceType.AGENT,
requiredPermissions: -1,
}),
).rejects.toThrow('requiredPermissions must be a positive number');
});
test('should return empty array for invalid resource type (error is caught)', async () => {
// The service catches invalid resourceType errors and returns empty array
const result = await service.findAccessibleResources({
userId,
resourceType: 'invalid' as ResourceType,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
});
describe('empty principals', () => {
test('should return empty array when no principals found', async () => {
mockGetUserPrincipals.mockResolvedValue([]);
const result = await service.findAccessibleResources({
userId,
resourceType: ResourceType.AGENT,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
});
describe('successful queries', () => {
test('should find resources user can view', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
]);
const viewableResources = await service.findAccessibleResources({
userId,
resourceType: ResourceType.AGENT,
requiredPermissions: 1, // VIEW
});
expect(viewableResources).toHaveLength(2);
const resourceIds = viewableResources.map((id) => id.toString());
expect(resourceIds).toContain(resource1.toString());
expect(resourceIds).toContain(resource2.toString());
});
test('should find resources user can edit', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
]);
const editableResources = await service.findAccessibleResources({
userId,
resourceType: ResourceType.AGENT,
requiredPermissions: 3, // EDIT
});
expect(editableResources).toHaveLength(1);
expect(editableResources[0].toString()).toBe(resource2.toString());
});
test('should find resources accessible via group membership', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.GROUP, principalId: groupId },
]);
const viewableResources = await service.findAccessibleResources({
userId,
resourceType: ResourceType.AGENT,
requiredPermissions: 1, // VIEW
});
expect(viewableResources).toHaveLength(3);
});
test('should pass role when provided', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
]);
await service.findAccessibleResources({
userId,
role: 'admin',
resourceType: ResourceType.AGENT,
requiredPermissions: 1,
});
expect(mockGetUserPrincipals).toHaveBeenCalledWith({
userId,
role: 'admin',
});
});
});
});
describe('findPubliclyAccessibleResources', () => {
const publicResource1 = new Types.ObjectId();
const publicResource2 = new Types.ObjectId();
const privateResource = new Types.ObjectId();
beforeEach(async () => {
// Public can view resource 1
await service.grantPermission({
principalType: PrincipalType.PUBLIC,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId: publicResource1,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
// Public can edit resource 2
await service.grantPermission({
principalType: PrincipalType.PUBLIC,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId: publicResource2,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
// Private resource - only user access
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: privateResource,
accessRoleId: AccessRoleIds.AGENT_OWNER,
grantedBy: grantedById,
});
});
describe('validation', () => {
test('should throw error when requiredPermissions is not a positive number', async () => {
await expect(
service.findPubliclyAccessibleResources({
resourceType: ResourceType.AGENT,
requiredPermissions: 0,
}),
).rejects.toThrow('requiredPermissions must be a positive number');
});
test('should return empty array for invalid resource type (error is caught)', async () => {
// The service catches invalid resourceType errors and returns empty array
const result = await service.findPubliclyAccessibleResources({
resourceType: 'invalid' as ResourceType,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
});
describe('finding public resources', () => {
test('should find publicly viewable resources', async () => {
const publicResources = await service.findPubliclyAccessibleResources({
resourceType: ResourceType.AGENT,
requiredPermissions: 1, // VIEW
});
expect(publicResources).toHaveLength(2);
const resourceIds = publicResources.map((id) => id.toString());
expect(resourceIds).toContain(publicResource1.toString());
expect(resourceIds).toContain(publicResource2.toString());
expect(resourceIds).not.toContain(privateResource.toString());
});
test('should find publicly editable resources', async () => {
const editableResources = await service.findPubliclyAccessibleResources({
resourceType: ResourceType.AGENT,
requiredPermissions: 3, // EDIT
});
expect(editableResources).toHaveLength(1);
expect(editableResources[0].toString()).toBe(publicResource2.toString());
});
test('should return empty array when no public permissions exist', async () => {
const noPublicResources = await service.findPubliclyAccessibleResources({
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: 1,
});
expect(noPublicResources).toEqual([]);
});
});
});
describe('getResourcePermissionsMap', () => {
const resource1 = new Types.ObjectId();
const resource2 = new Types.ObjectId();
const resource3 = new Types.ObjectId();
beforeEach(async () => {
// User has VIEW on resource1
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: resource1,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
// User has EDIT on resource2
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: resource2,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
// Group has EDIT on resource1 (higher permission)
await service.grantPermission({
principalType: PrincipalType.GROUP,
principalId: groupId,
resourceType: ResourceType.AGENT,
resourceId: resource1,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
// resource3 has no permissions
});
describe('empty arrays', () => {
test('should return empty map for empty resourceIds array', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
]);
const permissionsMap = await service.getResourcePermissionsMap({
userId,
role: 'user',
resourceType: ResourceType.AGENT,
resourceIds: [],
});
expect(permissionsMap).toBeInstanceOf(Map);
expect(permissionsMap.size).toBe(0);
});
test('should throw on invalid resource type', async () => {
await expect(
service.getResourcePermissionsMap({
userId,
role: 'user',
resourceType: 'invalid' as ResourceType,
resourceIds: [resource1],
}),
).rejects.toThrow('Invalid resourceType: invalid');
});
});
describe('batch queries', () => {
test('should get permissions for multiple resources in single query', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.PUBLIC },
]);
const permissionsMap = await service.getResourcePermissionsMap({
userId,
role: 'user',
resourceType: ResourceType.AGENT,
resourceIds: [resource1, resource2, resource3],
});
expect(permissionsMap).toBeInstanceOf(Map);
expect(permissionsMap.size).toBe(2); // resource1 and resource2
expect(permissionsMap.get(resource1.toString())).toBe(RoleBits.VIEWER);
expect(permissionsMap.get(resource2.toString())).toBe(RoleBits.EDITOR);
expect(permissionsMap.get(resource3.toString())).toBeUndefined();
});
test('should combine permissions from multiple principals', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.GROUP, principalId: groupId },
]);
const permissionsMap = await service.getResourcePermissionsMap({
userId,
role: 'user',
resourceType: ResourceType.AGENT,
resourceIds: [resource1, resource2],
});
expect(permissionsMap.size).toBe(2);
// Resource1 should have VIEW (1) | EDIT (3) = 3 from combined user+group
expect(permissionsMap.get(resource1.toString())).toBe(RoleBits.EDITOR);
expect(permissionsMap.get(resource2.toString())).toBe(RoleBits.EDITOR);
});
test('should use role optimization when provided', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
]);
await service.getResourcePermissionsMap({
userId,
role: 'admin',
resourceType: ResourceType.AGENT,
resourceIds: [resource1],
});
expect(mockGetUserPrincipals).toHaveBeenCalledWith({ userId, role: 'admin' });
});
});
});
describe('removeAllPermissions', () => {
const resourceToDelete = new Types.ObjectId();
beforeEach(async () => {
// Grant multiple permissions to the resource
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: resourceToDelete,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
await service.grantPermission({
principalType: PrincipalType.GROUP,
principalId: groupId,
resourceType: ResourceType.AGENT,
resourceId: resourceToDelete,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
await service.grantPermission({
principalType: PrincipalType.PUBLIC,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId: resourceToDelete,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
});
describe('validation', () => {
test('should throw error for invalid resource type', async () => {
await expect(
service.removeAllPermissions({
resourceType: 'invalid' as ResourceType,
resourceId: resourceToDelete,
}),
).rejects.toThrow('Invalid resourceType: invalid');
});
test('should throw error for invalid resource ID', async () => {
await expect(
service.removeAllPermissions({
resourceType: ResourceType.AGENT,
resourceId: 'invalid-id',
}),
).rejects.toThrow('Invalid resource ID: invalid-id');
});
});
describe('cleanup', () => {
test('should delete all permissions for a resource', async () => {
// Verify permissions exist
const beforeCount = await AclEntry.countDocuments({
resourceType: ResourceType.AGENT,
resourceId: resourceToDelete,
});
expect(beforeCount).toBe(3);
const result = await service.removeAllPermissions({
resourceType: ResourceType.AGENT,
resourceId: resourceToDelete,
});
expect(result.acknowledged).toBe(true);
expect(result.deletedCount).toBe(3);
// Verify permissions are deleted
const afterCount = await AclEntry.countDocuments({
resourceType: ResourceType.AGENT,
resourceId: resourceToDelete,
});
expect(afterCount).toBe(0);
});
test('should return result even when no permissions existed', async () => {
const newResourceId = new Types.ObjectId();
const result = await service.removeAllPermissions({
resourceType: ResourceType.AGENT,
resourceId: newResourceId,
});
expect(result.acknowledged).toBe(true);
expect(result.deletedCount).toBe(0);
});
test('should not affect other resources permissions', async () => {
const otherResource = new Types.ObjectId();
// Grant permission to another resource
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: otherResource,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
await service.removeAllPermissions({
resourceType: ResourceType.AGENT,
resourceId: resourceToDelete,
});
// Verify other resource still has permissions
const otherResourcePerms = await AclEntry.countDocuments({
resourceType: ResourceType.AGENT,
resourceId: otherResource,
});
expect(otherResourcePerms).toBe(1);
});
});
});
describe('checkPermission', () => {
const testResource = new Types.ObjectId();
const groupResource = new Types.ObjectId();
beforeEach(async () => {
// User has VIEW on testResource
await service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: ResourceType.AGENT,
resourceId: testResource,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
// Group has EDIT on groupResource
await service.grantPermission({
principalType: PrincipalType.GROUP,
principalId: groupId,
resourceType: ResourceType.AGENT,
resourceId: groupResource,
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
});
describe('validation', () => {
test('should throw error when requiredPermission is not a positive number', async () => {
await expect(
service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: testResource,
requiredPermission: 0,
}),
).rejects.toThrow('requiredPermission must be a positive number');
});
test('should throw error when requiredPermission is negative', async () => {
await expect(
service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: testResource,
requiredPermission: -1,
}),
).rejects.toThrow('requiredPermission must be a positive number');
});
test('should return false for invalid resource type (error is caught)', async () => {
// The service catches invalid resourceType errors and returns false
const hasPermission = await service.checkPermission({
userId: userId.toString(),
resourceType: 'invalid' as ResourceType,
resourceId: testResource,
requiredPermission: 1,
});
expect(hasPermission).toBe(false);
});
});
describe('permission scenarios', () => {
test('should return true when user has required permission', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
]);
const hasPermission = await service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: testResource,
requiredPermission: 1, // VIEW
});
expect(hasPermission).toBe(true);
});
test('should return false when user lacks required permission', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
]);
const hasPermission = await service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: testResource,
requiredPermission: 3, // EDIT
});
expect(hasPermission).toBe(false);
});
test('should return false when no principals found', async () => {
mockGetUserPrincipals.mockResolvedValue([]);
const hasPermission = await service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: testResource,
requiredPermission: 1,
});
expect(hasPermission).toBe(false);
});
test('should check permission via group membership', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.GROUP, principalId: groupId },
]);
const hasPermission = await service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: groupResource,
requiredPermission: 1, // VIEW (editor includes view)
});
expect(hasPermission).toBe(true);
});
test('should return false for non-existent resource', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
]);
const hasPermission = await service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: new Types.ObjectId(),
requiredPermission: 1,
});
expect(hasPermission).toBe(false);
});
test('should pass role when provided for optimization', async () => {
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
]);
await service.checkPermission({
userId: userId.toString(),
role: 'admin',
resourceType: ResourceType.AGENT,
resourceId: testResource,
requiredPermission: 1,
});
expect(mockGetUserPrincipals).toHaveBeenCalledWith({
userId: userId.toString(),
role: 'admin',
});
});
});
describe('public access', () => {
test('should check permission for public access', async () => {
const publicResource = new Types.ObjectId();
await service.grantPermission({
principalType: PrincipalType.PUBLIC,
principalId: null,
resourceType: ResourceType.AGENT,
resourceId: publicResource,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
mockGetUserPrincipals.mockResolvedValue([
{ principalType: PrincipalType.USER, principalId: userId },
{ principalType: PrincipalType.PUBLIC },
]);
const hasPermission = await service.checkPermission({
userId: userId.toString(),
resourceType: ResourceType.AGENT,
resourceId: publicResource,
requiredPermission: 1,
});
expect(hasPermission).toBe(true);
});
});
});
describe('validateResourceType (via public methods)', () => {
test('should accept AGENT resource type', async () => {
mockGetUserPrincipals.mockResolvedValue([]);
const result = await service.findAccessibleResources({
userId,
resourceType: ResourceType.AGENT,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
test('should accept PROMPTGROUP resource type', async () => {
mockGetUserPrincipals.mockResolvedValue([]);
const result = await service.findAccessibleResources({
userId,
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
test('should accept MCPSERVER resource type', async () => {
mockGetUserPrincipals.mockResolvedValue([]);
const result = await service.findAccessibleResources({
userId,
resourceType: ResourceType.MCPSERVER,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
test('should return empty array for unknown resource type (error caught)', async () => {
// findAccessibleResources catches invalid resource type and returns empty array
const result = await service.findAccessibleResources({
userId,
resourceType: 'unknown_type' as ResourceType,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
test('should return empty array for empty string resource type (error caught)', async () => {
// findAccessibleResources catches invalid resource type and returns empty array
const result = await service.findAccessibleResources({
userId,
resourceType: '' as ResourceType,
requiredPermissions: 1,
});
expect(result).toEqual([]);
});
test('should throw for invalid resource type in grantPermission', async () => {
// grantPermission throws directly for invalid resource type
await expect(
service.grantPermission({
principalType: PrincipalType.USER,
principalId: userId,
resourceType: 'unknown_type' as ResourceType,
resourceId,
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Invalid resourceType: unknown_type');
});
test('should throw for empty string resource type in removeAllPermissions', async () => {
// removeAllPermissions throws directly for invalid resource type
await expect(
service.removeAllPermissions({
resourceType: '' as ResourceType,
resourceId,
}),
).rejects.toThrow('Invalid resourceType:');
});
});
});