🔧 refactor: Integrate PrincipalModel Enum for Principal Handling

- Replaced string literals for principal models ('User', 'Group') with the new PrincipalModel enum across various models, services, and tests to enhance type safety and consistency.
- Updated permission handling in multiple files to utilize the PrincipalModel enum, improving maintainability and reducing potential errors.
- Ensured all relevant tests reflect these changes to maintain coverage and functionality.
This commit is contained in:
Danny Avila 2025-08-02 16:14:11 -04:00
parent 49d1cefe71
commit 28d63dab71
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
10 changed files with 61 additions and 30 deletions

View file

@ -7,6 +7,7 @@ const {
ResourceType, ResourceType,
AccessRoleIds, AccessRoleIds,
PrincipalType, PrincipalType,
PrincipalModel,
PermissionBits, PermissionBits,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
@ -211,7 +212,7 @@ describe('PromptGroup Migration Script', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: testOwner._id, principalId: testOwner._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.PROMPTGROUP, resourceType: ResourceType.PROMPTGROUP,
resourceId: promptGroup1._id, resourceId: promptGroup1._id,
permBits: ownerRole.permBits, permBits: ownerRole.permBits,

View file

@ -1,5 +1,5 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { ResourceType, PrincipalType } = require('librechat-data-provider'); const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider');
const { MongoMemoryServer } = require('mongodb-memory-server'); const { MongoMemoryServer } = require('mongodb-memory-server');
const { canAccessAgentResource } = require('./canAccessAgentResource'); const { canAccessAgentResource } = require('./canAccessAgentResource');
const { User, Role, AclEntry } = require('~/db/models'); const { User, Role, AclEntry } = require('~/db/models');
@ -99,7 +99,7 @@ describe('canAccessAgentResource middleware', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions (1+2+4+8) permBits: 15, // All permissions (1+2+4+8)
@ -136,7 +136,7 @@ describe('canAccessAgentResource middleware', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: otherUser._id, principalId: otherUser._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions permBits: 15, // All permissions
@ -177,7 +177,7 @@ describe('canAccessAgentResource middleware', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 1, // VIEW permission permBits: 1, // VIEW permission
@ -214,7 +214,7 @@ describe('canAccessAgentResource middleware', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 1, // VIEW permission only permBits: 1, // VIEW permission only
@ -261,7 +261,7 @@ describe('canAccessAgentResource middleware', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions permBits: 15, // All permissions
@ -297,7 +297,7 @@ describe('canAccessAgentResource middleware', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions (1+2+4+8) permBits: 15, // All permissions (1+2+4+8)
@ -357,7 +357,7 @@ describe('canAccessAgentResource middleware', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions permBits: 15, // All permissions

View file

@ -22,6 +22,16 @@ jest.mock('librechat-data-provider', () => ({
GROUP: 'group', GROUP: 'group',
PUBLIC: 'public', PUBLIC: 'public',
}, },
PrincipalModel: {
USER: 'User',
GROUP: 'Group',
},
ResourceType: {
AGENT: 'agent',
PROJECT: 'project',
FILE: 'file',
PROMPTGROUP: 'promptGroup',
},
FileContext: { message_attachment: 'message_attachment' }, FileContext: { message_attachment: 'message_attachment' },
FileSources: { local: 'local' }, FileSources: { local: 'local' },
EModelEndpoint: { assistants: 'assistants' }, EModelEndpoint: { assistants: 'assistants' },

View file

@ -1,6 +1,6 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { isEnabled } = require('@librechat/api'); const { isEnabled } = require('@librechat/api');
const { ResourceType, PrincipalType } = require('librechat-data-provider'); const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider');
const { getTransactionSupport, logger } = require('@librechat/data-schemas'); const { getTransactionSupport, logger } = require('@librechat/data-schemas');
const { const {
entraIdPrincipalFeatureEnabled, entraIdPrincipalFeatureEnabled,
@ -627,9 +627,10 @@ const bulkUpdateResourcePermissions = async ({
principalType: principal.type, principalType: principal.type,
resourceType, resourceType,
resourceId, resourceId,
...(principal.type !== 'public' && { ...(principal.type !== PrincipalType.PUBLIC && {
principalId: principal.id, principalId: principal.id,
principalModel: principal.type === 'user' ? 'User' : 'Group', principalModel:
principal.type === PrincipalType.USER ? PrincipalModel.USER : PrincipalModel.GROUP,
}), }),
}, },
}; };

View file

@ -1,7 +1,12 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { RoleBits, createModels } = require('@librechat/data-schemas'); const { RoleBits, createModels } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server'); const { MongoMemoryServer } = require('mongodb-memory-server');
const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider'); const {
ResourceType,
AccessRoleIds,
PrincipalType,
PrincipalModel,
} = require('librechat-data-provider');
const { const {
bulkUpdateResourcePermissions, bulkUpdateResourcePermissions,
getEffectivePermissions, getEffectivePermissions,
@ -87,10 +92,10 @@ describe('PermissionService', () => {
}); });
expect(entry).toBeDefined(); expect(entry).toBeDefined();
expect(entry.principalType).toBe('user'); expect(entry.principalType).toBe(PrincipalType.USER);
expect(entry.principalId.toString()).toBe(userId.toString()); expect(entry.principalId.toString()).toBe(userId.toString());
expect(entry.principalModel).toBe('User'); expect(entry.principalModel).toBe(PrincipalModel.USER);
expect(entry.resourceType).toBe('agent'); expect(entry.resourceType).toBe(ResourceType.AGENT);
expect(entry.resourceId.toString()).toBe(resourceId.toString()); expect(entry.resourceId.toString()).toBe(resourceId.toString());
// Get the role to verify the permission bits are correctly set // Get the role to verify the permission bits are correctly set
@ -114,7 +119,7 @@ describe('PermissionService', () => {
expect(entry).toBeDefined(); expect(entry).toBeDefined();
expect(entry.principalType).toBe(PrincipalType.GROUP); expect(entry.principalType).toBe(PrincipalType.GROUP);
expect(entry.principalId.toString()).toBe(groupId.toString()); expect(entry.principalId.toString()).toBe(groupId.toString());
expect(entry.principalModel).toBe('Group'); expect(entry.principalModel).toBe(PrincipalModel.GROUP);
// Get the role to verify the permission bits are correctly set // Get the role to verify the permission bits are correctly set
const role = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR); const role = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
@ -433,7 +438,7 @@ describe('PermissionService', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: userId, principalId: userId,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: childResourceId, resourceId: childResourceId,
permBits: RoleBits.VIEWER, permBits: RoleBits.VIEWER,

View file

@ -19,6 +19,14 @@ export enum PrincipalType {
PUBLIC = 'public', PUBLIC = 'public',
} }
/**
* Principal model types for MongoDB references
*/
export enum PrincipalModel {
USER = 'User',
GROUP = 'Group',
}
/** /**
* Source of the principal (local LibreChat or external Entra ID) * Source of the principal (local LibreChat or external Entra ID)
*/ */

View file

@ -1,5 +1,10 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { ResourceType, PermissionBits, PrincipalType } from 'librechat-data-provider'; import {
ResourceType,
PrincipalType,
PrincipalModel,
PermissionBits,
} from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server'; import { MongoMemoryServer } from 'mongodb-memory-server';
import type * as t from '~/types'; import type * as t from '~/types';
import { createAclEntryMethods } from './aclEntry'; import { createAclEntryMethods } from './aclEntry';
@ -47,7 +52,7 @@ describe('AclEntry Model Tests', () => {
expect(entry).toBeDefined(); expect(entry).toBeDefined();
expect(entry?.principalType).toBe(PrincipalType.USER); expect(entry?.principalType).toBe(PrincipalType.USER);
expect(entry?.principalId?.toString()).toBe(userId.toString()); expect(entry?.principalId?.toString()).toBe(userId.toString());
expect(entry?.principalModel).toBe('User'); expect(entry?.principalModel).toBe(PrincipalModel.USER);
expect(entry?.resourceType).toBe(ResourceType.AGENT); expect(entry?.resourceType).toBe(ResourceType.AGENT);
expect(entry?.resourceId.toString()).toBe(resourceId.toString()); expect(entry?.resourceId.toString()).toBe(resourceId.toString());
expect(entry?.permBits).toBe(PermissionBits.VIEW); expect(entry?.permBits).toBe(PermissionBits.VIEW);
@ -68,7 +73,7 @@ describe('AclEntry Model Tests', () => {
expect(entry).toBeDefined(); expect(entry).toBeDefined();
expect(entry?.principalType).toBe(PrincipalType.GROUP); expect(entry?.principalType).toBe(PrincipalType.GROUP);
expect(entry?.principalId?.toString()).toBe(groupId.toString()); expect(entry?.principalId?.toString()).toBe(groupId.toString());
expect(entry?.principalModel).toBe('Group'); expect(entry?.principalModel).toBe(PrincipalModel.GROUP);
expect(entry?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT); expect(entry?.permBits).toBe(PermissionBits.VIEW | PermissionBits.EDIT);
}); });
@ -469,7 +474,7 @@ describe('AclEntry Model Tests', () => {
await AclEntry.create({ await AclEntry.create({
principalType: PrincipalType.USER, principalType: PrincipalType.USER,
principalId: userId, principalId: userId,
principalModel: 'User', principalModel: PrincipalModel.USER,
resourceType: ResourceType.AGENT, resourceType: ResourceType.AGENT,
resourceId: childResourceId, resourceId: childResourceId,
permBits: PermissionBits.VIEW, permBits: PermissionBits.VIEW,

View file

@ -1,4 +1,4 @@
import { PrincipalType } from 'librechat-data-provider'; import { PrincipalType, PrincipalModel } from 'librechat-data-provider';
import type { Model, Types, DeleteResult, ClientSession } from 'mongoose'; import type { Model, Types, DeleteResult, ClientSession } from 'mongoose';
import type { IAclEntry } from '~/types'; import type { IAclEntry } from '~/types';
@ -148,7 +148,8 @@ export function createAclEntryMethods(mongoose: typeof import('mongoose')) {
if (principalType !== PrincipalType.PUBLIC) { if (principalType !== PrincipalType.PUBLIC) {
query.principalId = principalId; query.principalId = principalId;
query.principalModel = principalType === PrincipalType.USER ? 'User' : 'Group'; query.principalModel =
principalType === PrincipalType.USER ? PrincipalModel.USER : PrincipalModel.GROUP;
} }
const update = { const update = {

View file

@ -1,5 +1,5 @@
import { Schema } from 'mongoose'; import { Schema } from 'mongoose';
import { PrincipalType } from 'librechat-data-provider'; import { PrincipalType, PrincipalModel, ResourceType } from 'librechat-data-provider';
import type { IAclEntry } from '~/types'; import type { IAclEntry } from '~/types';
const aclEntrySchema = new Schema<IAclEntry>( const aclEntrySchema = new Schema<IAclEntry>(
@ -19,14 +19,14 @@ const aclEntrySchema = new Schema<IAclEntry>(
}, },
principalModel: { principalModel: {
type: String, type: String,
enum: ['User', 'Group'], enum: Object.values(PrincipalModel),
required: function (this: IAclEntry) { required: function (this: IAclEntry) {
return this.principalType !== PrincipalType.PUBLIC; return this.principalType !== PrincipalType.PUBLIC;
}, },
}, },
resourceType: { resourceType: {
type: String, type: String,
enum: ['agent', 'project', 'file', 'prompt', 'promptGroup'], enum: Object.values(ResourceType),
required: true, required: true,
}, },
resourceId: { resourceId: {

View file

@ -1,5 +1,5 @@
import type { Document, Types } from 'mongoose'; import type { Document, Types } from 'mongoose';
import { PrincipalType } from 'librechat-data-provider'; import { PrincipalType, PrincipalModel, ResourceType } from 'librechat-data-provider';
export type AclEntry = { export type AclEntry = {
/** The type of principal ('user', 'group', 'public') */ /** The type of principal ('user', 'group', 'public') */
@ -7,9 +7,9 @@ export type AclEntry = {
/** The ID of the principal (null for 'public') */ /** The ID of the principal (null for 'public') */
principalId?: Types.ObjectId; principalId?: Types.ObjectId;
/** The model name for the principal ('User' or 'Group') */ /** The model name for the principal ('User' or 'Group') */
principalModel?: 'User' | 'Group'; principalModel?: PrincipalModel;
/** The type of resource ('agent', 'project', 'file', 'promptGroup') */ /** The type of resource ('agent', 'project', 'file', 'promptGroup') */
resourceType: 'agent' | 'project' | 'file' | 'promptGroup'; resourceType: ResourceType;
/** The ID of the resource */ /** The ID of the resource */
resourceId: Types.ObjectId; resourceId: Types.ObjectId;
/** Permission bits for this entry */ /** Permission bits for this entry */