mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-12 11:02:37 +01:00
408 lines
12 KiB
JavaScript
408 lines
12 KiB
JavaScript
|
|
const mongoose = require('mongoose');
|
||
|
|
const { createModels, createMethods } = require('@librechat/data-schemas');
|
||
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||
|
|
const { SystemRoles, PrincipalType } = require('librechat-data-provider');
|
||
|
|
const { SystemCapabilities } = require('@librechat/data-schemas');
|
||
|
|
|
||
|
|
jest.mock('@librechat/data-schemas', () => ({
|
||
|
|
...jest.requireActual('@librechat/data-schemas'),
|
||
|
|
getTransactionSupport: jest.fn().mockResolvedValue(false),
|
||
|
|
createModels: jest.requireActual('@librechat/data-schemas').createModels,
|
||
|
|
createMethods: jest.requireActual('@librechat/data-schemas').createMethods,
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/server/services/GraphApiService', () => ({
|
||
|
|
entraIdPrincipalFeatureEnabled: jest.fn().mockReturnValue(false),
|
||
|
|
getUserOwnedEntraGroups: jest.fn().mockResolvedValue([]),
|
||
|
|
getUserEntraGroups: jest.fn().mockResolvedValue([]),
|
||
|
|
getGroupMembers: jest.fn().mockResolvedValue([]),
|
||
|
|
getGroupOwners: jest.fn().mockResolvedValue([]),
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.mock('~/config', () => ({
|
||
|
|
logger: { error: jest.fn() },
|
||
|
|
}));
|
||
|
|
|
||
|
|
let mongoServer;
|
||
|
|
let methods;
|
||
|
|
let SystemGrant;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
mongoServer = await MongoMemoryServer.create();
|
||
|
|
await mongoose.connect(mongoServer.getUri());
|
||
|
|
|
||
|
|
createModels(mongoose);
|
||
|
|
const dbModels = require('~/db/models');
|
||
|
|
Object.assign(mongoose.models, dbModels);
|
||
|
|
SystemGrant = dbModels.SystemGrant;
|
||
|
|
|
||
|
|
methods = createMethods(mongoose, {
|
||
|
|
matchModelName: () => null,
|
||
|
|
findMatchingPattern: () => null,
|
||
|
|
getCache: () => ({
|
||
|
|
get: async () => null,
|
||
|
|
set: async () => {},
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await mongoose.disconnect();
|
||
|
|
await mongoServer.stop();
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
await SystemGrant.deleteMany({});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('SystemGrant methods', () => {
|
||
|
|
describe('seedSystemGrants', () => {
|
||
|
|
it('seeds all capabilities for the ADMIN role', async () => {
|
||
|
|
await methods.seedSystemGrants();
|
||
|
|
|
||
|
|
const grants = await SystemGrant.find({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.ADMIN,
|
||
|
|
}).lean();
|
||
|
|
|
||
|
|
const expectedCount = Object.values(SystemCapabilities).length;
|
||
|
|
expect(grants).toHaveLength(expectedCount);
|
||
|
|
|
||
|
|
const capabilities = grants.map((g) => g.capability).sort();
|
||
|
|
const expected = Object.values(SystemCapabilities).sort();
|
||
|
|
expect(capabilities).toEqual(expected);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('is idempotent — calling twice does not duplicate grants', async () => {
|
||
|
|
await methods.seedSystemGrants();
|
||
|
|
await methods.seedSystemGrants();
|
||
|
|
|
||
|
|
const count = await SystemGrant.countDocuments({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.ADMIN,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(count).toBe(Object.values(SystemCapabilities).length);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('seeds grants with no tenantId', async () => {
|
||
|
|
await methods.seedSystemGrants();
|
||
|
|
|
||
|
|
const withTenant = await SystemGrant.countDocuments({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.ADMIN,
|
||
|
|
tenantId: { $exists: true },
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(withTenant).toBe(0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('grantCapability / revokeCapability', () => {
|
||
|
|
it('grants a capability to a user', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
});
|
||
|
|
|
||
|
|
const grant = await SystemGrant.findOne({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
}).lean();
|
||
|
|
|
||
|
|
expect(grant).toBeTruthy();
|
||
|
|
expect(grant.grantedAt).toBeInstanceOf(Date);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('upsert does not create duplicates', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
});
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
});
|
||
|
|
|
||
|
|
const count = await SystemGrant.countDocuments({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(count).toBe(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('revokes a capability', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
});
|
||
|
|
|
||
|
|
await methods.revokeCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
});
|
||
|
|
|
||
|
|
const grant = await SystemGrant.findOne({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_USERS,
|
||
|
|
}).lean();
|
||
|
|
|
||
|
|
expect(grant).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('hasCapabilityForPrincipals', () => {
|
||
|
|
it('returns true when role principal has the capability', async () => {
|
||
|
|
await methods.seedSystemGrants();
|
||
|
|
|
||
|
|
const principals = [
|
||
|
|
{ principalType: PrincipalType.USER, principalId: new mongoose.Types.ObjectId() },
|
||
|
|
{ principalType: PrincipalType.ROLE, principalId: SystemRoles.ADMIN },
|
||
|
|
{ principalType: PrincipalType.PUBLIC },
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals,
|
||
|
|
capability: SystemCapabilities.ACCESS_ADMIN,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns false when no principal has the capability', async () => {
|
||
|
|
const principals = [
|
||
|
|
{ principalType: PrincipalType.USER, principalId: new mongoose.Types.ObjectId() },
|
||
|
|
{ principalType: PrincipalType.ROLE, principalId: SystemRoles.USER },
|
||
|
|
{ principalType: PrincipalType.PUBLIC },
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals,
|
||
|
|
capability: SystemCapabilities.ACCESS_ADMIN,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns false for an empty principals list', async () => {
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals: [],
|
||
|
|
capability: SystemCapabilities.ACCESS_ADMIN,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('ignores PUBLIC principals', async () => {
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals: [{ principalType: PrincipalType.PUBLIC }],
|
||
|
|
capability: SystemCapabilities.ACCESS_ADMIN,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('matches user-level grants', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
});
|
||
|
|
|
||
|
|
const principals = [
|
||
|
|
{ principalType: PrincipalType.USER, principalId: userId },
|
||
|
|
{ principalType: PrincipalType.ROLE, principalId: SystemRoles.USER },
|
||
|
|
{ principalType: PrincipalType.PUBLIC },
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals,
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('matches group-level grants', async () => {
|
||
|
|
const groupId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.GROUP,
|
||
|
|
principalId: groupId,
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
const principals = [
|
||
|
|
{ principalType: PrincipalType.USER, principalId: new mongoose.Types.ObjectId() },
|
||
|
|
{ principalType: PrincipalType.GROUP, principalId: groupId },
|
||
|
|
{ principalType: PrincipalType.PUBLIC },
|
||
|
|
];
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals,
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getCapabilitiesForPrincipal', () => {
|
||
|
|
it('lists all capabilities for a principal', async () => {
|
||
|
|
await methods.seedSystemGrants();
|
||
|
|
|
||
|
|
const grants = await methods.getCapabilitiesForPrincipal({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.ADMIN,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(grants).toHaveLength(Object.values(SystemCapabilities).length);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns empty array for a principal with no grants', async () => {
|
||
|
|
const grants = await methods.getCapabilitiesForPrincipal({
|
||
|
|
principalType: PrincipalType.ROLE,
|
||
|
|
principalId: SystemRoles.USER,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(grants).toHaveLength(0);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('principalId normalization', () => {
|
||
|
|
it('grant with string userId is found by hasCapabilityForPrincipals with ObjectId', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId.toString(), // string input
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals: [{ principalType: PrincipalType.USER, principalId: userId }], // ObjectId input
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('revoke with string userId removes the grant stored as ObjectId', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId.toString(),
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
await methods.revokeCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId.toString(), // string revoke
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals: [{ principalType: PrincipalType.USER, principalId: userId }],
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('getCapabilitiesForPrincipal with string userId returns grants stored as ObjectId', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId.toString(),
|
||
|
|
capability: SystemCapabilities.READ_USAGE,
|
||
|
|
});
|
||
|
|
|
||
|
|
const grants = await methods.getCapabilitiesForPrincipal({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId.toString(), // string lookup
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(grants).toHaveLength(1);
|
||
|
|
expect(grants[0].capability).toBe(SystemCapabilities.READ_USAGE);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('tenant scoping', () => {
|
||
|
|
it('tenant-scoped grant does not match platform-level query', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
tenantId: 'tenant-1',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals: [{ principalType: PrincipalType.USER, principalId: userId }],
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('tenant-scoped grant matches same-tenant query', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
tenantId: 'tenant-1',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals: [{ principalType: PrincipalType.USER, principalId: userId }],
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
tenantId: 'tenant-1',
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('tenant-scoped grant does not match different tenant', async () => {
|
||
|
|
const userId = new mongoose.Types.ObjectId();
|
||
|
|
|
||
|
|
await methods.grantCapability({
|
||
|
|
principalType: PrincipalType.USER,
|
||
|
|
principalId: userId,
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
tenantId: 'tenant-1',
|
||
|
|
});
|
||
|
|
|
||
|
|
const result = await methods.hasCapabilityForPrincipals({
|
||
|
|
principals: [{ principalType: PrincipalType.USER, principalId: userId }],
|
||
|
|
capability: SystemCapabilities.READ_CONFIGS,
|
||
|
|
tenantId: 'tenant-2',
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(result).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|