🔧 refactor: Organize Sharing/Agent Components and Improve Type Safety

refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids, rename enums to PascalCase

refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids

chore: move sharing related components to dedicated "Sharing" directory

chore: remove PublicSharingToggle component and update index exports

chore: move non-sidepanel agent components to `~/components/Agents`

chore: move AgentCategoryDisplay component with tests

chore: remove commented out code

refactor: change PERMISSION_BITS from const to enum for better type safety

refactor: reorganize imports in GenericGrantAccessDialog and update index exports for hooks

refactor: update type definitions to use ACCESS_ROLE_IDS for improved type safety

refactor: remove unused canAccessPromptResource middleware and related code

refactor: remove unused prompt access roles from createAccessRoleMethods

refactor: update resourceType in AclEntry type definition to remove unused 'prompt' value

refactor: introduce ResourceType enum and update resourceType usage across data provider files for improved type safety

refactor: update resourceType usage to ResourceType enum across sharing and permissions components for improved type safety

refactor: standardize resourceType usage to ResourceType enum across agent and prompt models, permissions controller, and middleware for enhanced type safety

refactor: update resourceType references from PROMPT_GROUP to PROMPTGROUP for consistency across models, middleware, and components

refactor: standardize access role IDs and resource type usage across agent, file, and prompt models for improved type safety and consistency

chore: add typedefs for TUpdateResourcePermissionsRequest and TUpdateResourcePermissionsResponse to enhance type definitions

chore: move SearchPicker to PeoplePicker dir

refactor: implement debouncing for query changes in SearchPicker for improved performance

chore: fix typing, import order for agent admin settings

fix: agent admin settings, prevent agent form submission

refactor: rename `ACCESS_ROLE_IDS` to `AccessRoleIds`

refactor: replace PermissionBits with PERMISSION_BITS

refactor: replace PERMISSION_BITS with PermissionBits
This commit is contained in:
Danny Avila 2025-07-28 17:52:36 -04:00
parent ae732b2ebc
commit 81b32e400a
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
96 changed files with 781 additions and 798 deletions

View file

@ -1,23 +1,19 @@
const mongoose = require('mongoose');
const crypto = require('node:crypto');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles, Tools, actionDelimiter } = require('librechat-data-provider');
const { ResourceType, SystemRoles, Tools, actionDelimiter } = require('librechat-data-provider');
const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } =
require('librechat-data-provider').Constants;
const {
getProjectByName,
addAgentIdsToProject,
removeAgentIdsFromProject,
removeAgentFromAllProjects,
removeAgentIdsFromProject,
addAgentIdsToProject,
getProjectByName,
} = require('./Project');
const { getCachedTools } = require('~/server/services/Config');
const { removeAllPermissions } = require('~/server/services/PermissionService');
const { Agent } = require('~/db/models');
/**
* Category values are now imported from shared constants
*/
const { getCachedTools } = require('~/server/services/Config');
const { getActions } = require('./Action');
const { Agent } = require('~/db/models');
/**
* Create an agent with the provided data.
@ -511,7 +507,7 @@ const deleteAgent = async (searchParameter) => {
if (agent) {
await removeAgentFromAllProjects(agent.id);
await removeAllPermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
});
}

View file

@ -14,6 +14,7 @@ const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { agentSchema } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
const {
getAgent,
loadAgent,
@ -21,14 +22,14 @@ const {
updateAgent,
deleteAgent,
getListAgents,
revertAgentVersion,
updateAgentProjects,
addAgentResourceFile,
removeAgentResourceFiles,
generateActionMetadataHash,
revertAgentVersion,
} = require('./Agent');
const { getCachedTools } = require('~/server/services/Config');
const permissionService = require('~/server/services/PermissionService');
const { getCachedTools } = require('~/server/services/Config');
const { AclEntry } = require('~/db/models');
/**
@ -423,10 +424,10 @@ describe('models/Agent', () => {
// Create necessary access roles for agents
await AccessRole.create({
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
name: 'Owner',
description: 'Full control over agents',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: 15, // VIEW | EDIT | DELETE | SHARE
});
}, 20000);
@ -501,15 +502,15 @@ describe('models/Agent', () => {
await permissionService.grantPermission({
principalType: 'user',
principalId: authorId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
grantedBy: authorId,
});
// Verify ACL entry exists
const aclEntriesBefore = await AclEntry.find({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
});
expect(aclEntriesBefore).toHaveLength(1);
@ -523,7 +524,7 @@ describe('models/Agent', () => {
// Verify ACL entries are removed
const aclEntriesAfter = await AclEntry.find({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
});
expect(aclEntriesAfter).toHaveLength(0);

View file

@ -1,11 +1,12 @@
const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { createModels } = require('@librechat/data-schemas');
const { getFiles, createFile } = require('./File');
const { createAgent } = require('./Agent');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
const { grantPermission } = require('~/server/services/PermissionService');
const { getFiles, createFile } = require('./File');
const { seedDefaultRoles } = require('~/models');
const { createAgent } = require('./Agent');
let File;
let Agent;
@ -116,9 +117,9 @@ describe('File Access Control', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: authorId,
});
@ -233,9 +234,9 @@ describe('File Access Control', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: authorId,
});
@ -291,9 +292,9 @@ describe('File Access Control', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: authorId,
});

View file

@ -1,11 +1,16 @@
const { ObjectId } = require('mongodb');
const { logger } = require('@librechat/data-schemas');
const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider');
const {
getProjectByName,
addGroupIdsToProject,
removeGroupIdsFromProject,
Constants,
SystemRoles,
ResourceType,
SystemCategories,
} = require('librechat-data-provider');
const {
removeGroupFromAllProjects,
removeGroupIdsFromProject,
addGroupIdsToProject,
getProjectByName,
} = require('./Project');
const { removeAllPermissions } = require('~/server/services/PermissionService');
const { PromptGroup, Prompt } = require('~/db/models');
@ -234,7 +239,7 @@ const deletePromptGroup = async ({ _id, author, role }) => {
await removeGroupFromAllProjects(_id);
try {
await removeAllPermissions({ resourceType: 'promptGroup', resourceId: _id });
await removeAllPermissions({ resourceType: ResourceType.PROMPTGROUP, resourceId: _id });
} catch (error) {
logger.error('Error removing promptGroup permissions:', error);
}
@ -428,16 +433,6 @@ module.exports = {
throw new Error('Failed to delete the prompt');
}
// Remove all ACL entries for this prompt
try {
await removeAllPermissions({
resourceType: 'prompt',
resourceId: promptId,
});
} catch (error) {
logger.error('Error removing prompt permissions:', error);
}
const remainingPrompts = await Prompt.find({ groupId })
.select('_id')
.sort({ createdAt: 1 })
@ -447,7 +442,7 @@ module.exports = {
// Remove all ACL entries for the promptGroup when deleting the last prompt
try {
await removeAllPermissions({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: groupId,
});
} catch (error) {

View file

@ -1,8 +1,13 @@
const { ObjectId } = require('mongodb');
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
const { SystemRoles } = require('librechat-data-provider');
const { logger, PermissionBits } = require('@librechat/data-schemas');
const { ObjectId } = require('mongodb');
const { logger } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const {
SystemRoles,
ResourceType,
AccessRoleIds,
PermissionBits,
} = require('librechat-data-provider');
// Mock the config/connect module to prevent connection attempts during tests
jest.mock('../../config/connect', () => jest.fn().mockResolvedValue(true));
@ -49,24 +54,24 @@ async function setupTestData() {
// Create access roles for promptGroups
testRoles = {
viewer: await AccessRole.create({
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
name: 'Viewer',
description: 'Can view promptGroups',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits: PermissionBits.VIEW,
}),
editor: await AccessRole.create({
accessRoleId: 'promptGroup_editor',
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
name: 'Editor',
description: 'Can view and edit promptGroups',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits: PermissionBits.VIEW | PermissionBits.EDIT,
}),
owner: await AccessRole.create({
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
name: 'Owner',
description: 'Full control over promptGroups',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits:
PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE,
}),
@ -148,15 +153,15 @@ describe('Prompt ACL Permissions', () => {
await permissionService.grantPermission({
principalType: 'user',
principalId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: testUsers.owner._id,
});
// Check ACL entry
const aclEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
principalType: 'user',
principalId: testUsers.owner._id,
@ -192,9 +197,9 @@ describe('Prompt ACL Permissions', () => {
await permissionService.grantPermission({
principalType: 'user',
principalId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: testUsers.owner._id,
});
});
@ -208,7 +213,7 @@ describe('Prompt ACL Permissions', () => {
it('owner should have full access to their prompt', async () => {
const hasAccess = await permissionService.checkPermission({
userId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.VIEW,
});
@ -217,7 +222,7 @@ describe('Prompt ACL Permissions', () => {
const canEdit = await permissionService.checkPermission({
userId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.EDIT,
});
@ -230,22 +235,22 @@ describe('Prompt ACL Permissions', () => {
await permissionService.grantPermission({
principalType: 'user',
principalId: testUsers.viewer._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
grantedBy: testUsers.owner._id,
});
const canView = await permissionService.checkPermission({
userId: testUsers.viewer._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.VIEW,
});
const canEdit = await permissionService.checkPermission({
userId: testUsers.viewer._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.EDIT,
});
@ -257,7 +262,7 @@ describe('Prompt ACL Permissions', () => {
it('user without permissions should have no access', async () => {
const hasAccess = await permissionService.checkPermission({
userId: testUsers.noAccess._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.VIEW,
});
@ -270,7 +275,7 @@ describe('Prompt ACL Permissions', () => {
// The middleware layer handles admin bypass, not the permission service
const hasAccess = await permissionService.checkPermission({
userId: testUsers.admin._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.VIEW,
});
@ -278,7 +283,7 @@ describe('Prompt ACL Permissions', () => {
// Without explicit permissions, even admin won't have access at this layer
expect(hasAccess).toBe(false);
// The actual admin bypass happens in the middleware layer (canAccessPromptResource)
// The actual admin bypass happens in the middleware layer (`canAccessPromptViaGroup`/`canAccessPromptGroupResource`)
// which checks req.user.role === SystemRoles.ADMIN
});
});
@ -352,16 +357,16 @@ describe('Prompt ACL Permissions', () => {
await permissionService.grantPermission({
principalType: 'group',
principalId: testGroups.editors._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
accessRoleId: 'promptGroup_editor',
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
grantedBy: testUsers.owner._id,
});
// Check if group member has access
const hasAccess = await permissionService.checkPermission({
userId: testUsers.editor._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.EDIT,
});
@ -371,7 +376,7 @@ describe('Prompt ACL Permissions', () => {
// Check that non-member doesn't have access
const nonMemberAccess = await permissionService.checkPermission({
userId: testUsers.viewer._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
requiredPermission: PermissionBits.EDIT,
});
@ -420,9 +425,9 @@ describe('Prompt ACL Permissions', () => {
await permissionService.grantPermission({
principalType: 'public',
principalId: null,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: publicPromptGroup._id,
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
grantedBy: testUsers.owner._id,
});
@ -430,9 +435,9 @@ describe('Prompt ACL Permissions', () => {
await permissionService.grantPermission({
principalType: 'user',
principalId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: privatePromptGroup._id,
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: testUsers.owner._id,
});
});
@ -446,7 +451,7 @@ describe('Prompt ACL Permissions', () => {
it('public prompt should be accessible to any user', async () => {
const hasAccess = await permissionService.checkPermission({
userId: testUsers.noAccess._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: publicPromptGroup._id,
requiredPermission: PermissionBits.VIEW,
includePublic: true,
@ -458,7 +463,7 @@ describe('Prompt ACL Permissions', () => {
it('private prompt should not be accessible to unauthorized users', async () => {
const hasAccess = await permissionService.checkPermission({
userId: testUsers.noAccess._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: privatePromptGroup._id,
requiredPermission: PermissionBits.VIEW,
includePublic: true,
@ -501,15 +506,15 @@ describe('Prompt ACL Permissions', () => {
await permissionService.grantPermission({
principalType: 'user',
principalId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: testUsers.owner._id,
});
// Verify ACL entry exists
const beforeDelete = await AclEntry.find({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
});
expect(beforeDelete).toHaveLength(1);
@ -524,7 +529,7 @@ describe('Prompt ACL Permissions', () => {
// Verify ACL entries are removed
const aclEntries = await AclEntry.find({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testPromptGroup._id,
});

View file

@ -1,8 +1,13 @@
const { ObjectId } = require('mongodb');
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
const { logger, PermissionBits } = require('@librechat/data-schemas');
const { Constants } = require('librechat-data-provider');
const { ObjectId } = require('mongodb');
const { logger } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const {
Constants,
ResourceType,
AccessRoleIds,
PermissionBits,
} = require('librechat-data-provider');
// Mock the config/connect module to prevent connection attempts during tests
jest.mock('../../config/connect', () => jest.fn().mockResolvedValue(true));
@ -49,27 +54,27 @@ describe('PromptGroup Migration Script', () => {
// Create promptGroup access roles
ownerRole = await AccessRole.create({
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
name: 'Owner',
description: 'Full control over promptGroups',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits:
PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE,
});
viewerRole = await AccessRole.create({
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
name: 'Viewer',
description: 'Can view promptGroups',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits: PermissionBits.VIEW,
});
await AccessRole.create({
accessRoleId: 'promptGroup_editor',
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
name: 'Editor',
description: 'Can view and edit promptGroups',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits: PermissionBits.VIEW | PermissionBits.EDIT,
});
@ -103,7 +108,7 @@ describe('PromptGroup Migration Script', () => {
});
// Create private prompt group (not in any project)
const privatePromptGroup = await PromptGroup.create({
await PromptGroup.create({
name: 'Private Group',
author: testOwner._id,
authorName: testOwner.name,
@ -151,7 +156,7 @@ describe('PromptGroup Migration Script', () => {
// Check global promptGroup permissions
const globalOwnerEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: globalPromptGroup._id,
principalType: 'user',
principalId: testOwner._id,
@ -160,7 +165,7 @@ describe('PromptGroup Migration Script', () => {
expect(globalOwnerEntry.permBits).toBe(ownerRole.permBits);
const globalPublicEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: globalPromptGroup._id,
principalType: 'public',
});
@ -169,7 +174,7 @@ describe('PromptGroup Migration Script', () => {
// Check private promptGroup permissions
const privateOwnerEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: privatePromptGroup._id,
principalType: 'user',
principalId: testOwner._id,
@ -178,7 +183,7 @@ describe('PromptGroup Migration Script', () => {
expect(privateOwnerEntry.permBits).toBe(ownerRole.permBits);
const privatePublicEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: privatePromptGroup._id,
principalType: 'public',
});
@ -206,7 +211,7 @@ describe('PromptGroup Migration Script', () => {
principalType: 'user',
principalId: testOwner._id,
principalModel: 'User',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: promptGroup1._id,
permBits: ownerRole.permBits,
roleId: ownerRole._id,
@ -222,7 +227,7 @@ describe('PromptGroup Migration Script', () => {
// Verify promptGroup2 now has permissions
const group2Entry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: promptGroup2._id,
});
expect(group2Entry).toBeTruthy();
@ -259,7 +264,7 @@ describe('PromptGroup Migration Script', () => {
// Verify the promptGroup has permissions
const groupEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: promptGroup._id,
});
expect(groupEntry).toBeTruthy();

View file

@ -4,12 +4,13 @@
const mongoose = require('mongoose');
const { logger } = require('@librechat/data-schemas');
const { ResourceType } = require('librechat-data-provider');
const {
getAvailableRoles,
ensurePrincipalExists,
getEffectivePermissions,
ensureGroupPrincipalExists,
bulkUpdateResourcePermissions,
ensureGroupPrincipalExists,
getEffectivePermissions,
ensurePrincipalExists,
getAvailableRoles,
} = require('~/server/services/PermissionService');
const { AclEntry } = require('~/db/models');
const {
@ -18,8 +19,8 @@ const {
calculateRelevanceScore,
} = require('~/models');
const {
searchEntraIdPrincipals,
entraIdPrincipalFeatureEnabled,
searchEntraIdPrincipals,
} = require('~/server/services/GraphApiService');
/**
@ -27,6 +28,18 @@ const {
* Delegates validation and logic to PermissionService
*/
/**
* Validates that the resourceType is one of the supported enum values
* @param {string} resourceType - The resource type to validate
* @throws {Error} If resourceType is not valid
*/
const validateResourceType = (resourceType) => {
const validTypes = Object.values(ResourceType);
if (!validTypes.includes(resourceType)) {
throw new Error(`Invalid resourceType: ${resourceType}. Valid types: ${validTypes.join(', ')}`);
}
};
/**
* Bulk update permissions for a resource (grant, update, remove)
* @route PUT /api/{resourceType}/{resourceId}/permissions
@ -41,6 +54,8 @@ const {
const updateResourcePermissions = async (req, res) => {
try {
const { resourceType, resourceId } = req.params;
validateResourceType(resourceType);
/** @type {TUpdateResourcePermissionsRequest} */
const { updated, removed, public: isPublic, publicAccessRoleId } = req.body;
const { id: userId } = req.user;
@ -163,6 +178,7 @@ const updateResourcePermissions = async (req, res) => {
const getResourcePermissions = async (req, res) => {
try {
const { resourceType, resourceId } = req.params;
validateResourceType(resourceType);
// Use aggregation pipeline for efficient single-query data retrieval
const results = await AclEntry.aggregate([
@ -278,6 +294,7 @@ const getResourcePermissions = async (req, res) => {
const getResourceRoles = async (req, res) => {
try {
const { resourceType } = req.params;
validateResourceType(resourceType);
const roles = await getAvailableRoles({ resourceType });
@ -305,6 +322,8 @@ const getResourceRoles = async (req, res) => {
const getUserEffectivePermissions = async (req, res) => {
try {
const { resourceType, resourceId } = req.params;
validateResourceType(resourceType);
const { id: userId } = req.user;
const permissionBits = await getEffectivePermissions({

View file

@ -1,30 +1,33 @@
const { z } = require('zod');
const fs = require('fs').promises;
const { nanoid } = require('nanoid');
const { logger, PermissionBits } = require('@librechat/data-schemas');
const { logger } = require('@librechat/data-schemas');
const { agentCreateSchema, agentUpdateSchema } = require('@librechat/api');
const {
Tools,
SystemRoles,
FileSources,
ResourceType,
AccessRoleIds,
EToolResources,
actionDelimiter,
PermissionBits,
removeNullishValues,
} = require('librechat-data-provider');
const {
getAgent,
createAgent,
updateAgent,
deleteAgent,
getListAgentsByAccess,
countPromotedAgents,
revertAgentVersion,
createAgent,
updateAgent,
deleteAgent,
getAgent,
} = require('~/models/Agent');
const {
grantPermission,
findAccessibleResources,
findPubliclyAccessibleResources,
findAccessibleResources,
hasPublicPermission,
grantPermission,
} = require('~/server/services/PermissionService');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { resizeAvatar } = require('~/server/services/Files/images/avatar');
@ -79,9 +82,9 @@ const createAgentHandler = async (req, res) => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
grantedBy: userId,
});
logger.debug(
@ -146,7 +149,7 @@ const getAgentHandler = async (req, res, expandProperties = false) => {
// Check if agent is public
const isPublic = await hasPublicPermission({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
requiredPermissions: PermissionBits.VIEW,
});
@ -345,9 +348,9 @@ const duplicateAgentHandler = async (req, res) => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: newAgent._id,
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
grantedBy: userId,
});
logger.debug(
@ -440,11 +443,11 @@ const getListAgentsHandler = async (req, res) => {
// Get agent IDs the user has VIEW access to via ACL
const accessibleIds = await findAccessibleResources({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: requiredPermission,
});
const publiclyAccessibleIds = await findPubliclyAccessibleResources({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: PermissionBits.VIEW,
});
// Use the new ACL-aware function

View file

@ -1,5 +1,5 @@
const { logger } = require('@librechat/data-schemas');
const { Constants, isAgentsEndpoint } = require('librechat-data-provider');
const { Constants, isAgentsEndpoint, ResourceType } = require('librechat-data-provider');
const { canAccessResource } = require('./canAccessResource');
const { getAgent } = require('~/models/Agent');
@ -67,7 +67,7 @@ const canAccessAgentFromBody = (options) => {
}
const agentAccessMiddleware = canAccessResource({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermission,
resourceIdParam: 'agent_id', // This will be ignored since we use custom resolver
idResolver: () => resolveAgentIdFromBody(agentId),

View file

@ -1,5 +1,6 @@
const { getAgent } = require('~/models/Agent');
const { ResourceType } = require('librechat-data-provider');
const { canAccessResource } = require('./canAccessResource');
const { getAgent } = require('~/models/Agent');
/**
* Agent ID resolver function
@ -46,7 +47,7 @@ const canAccessAgentResource = (options) => {
}
return canAccessResource({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermission,
resourceIdParam,
idResolver: resolveAgentId,

View file

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

View file

@ -1,5 +1,6 @@
const { getPromptGroup } = require('~/models/Prompt');
const { ResourceType } = require('librechat-data-provider');
const { canAccessResource } = require('./canAccessResource');
const { getPromptGroup } = require('~/models/Prompt');
/**
* PromptGroup ID resolver function
@ -48,7 +49,7 @@ const canAccessPromptGroupResource = (options) => {
}
return canAccessResource({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
requiredPermission,
resourceIdParam,
idResolver: resolvePromptGroupId,

View file

@ -1,58 +0,0 @@
const { getPrompt } = require('~/models/Prompt');
const { canAccessResource } = require('./canAccessResource');
/**
* Prompt ID resolver function
* Resolves prompt ID to MongoDB ObjectId
*
* @param {string} promptId - Prompt ID from route parameter
* @returns {Promise<Object|null>} Prompt document with _id field, or null if not found
*/
const resolvePromptId = async (promptId) => {
return await getPrompt({ _id: promptId });
};
/**
* Prompt-specific middleware factory that creates middleware to check prompt access permissions.
* This middleware extends the generic canAccessResource to handle prompt ID resolution.
*
* @param {Object} options - Configuration options
* @param {number} options.requiredPermission - The permission bit required (1=view, 2=edit, 4=delete, 8=share)
* @param {string} [options.resourceIdParam='promptId'] - The name of the route parameter containing the prompt ID
* @returns {Function} Express middleware function
*
* @example
* // Basic usage for viewing prompts
* router.get('/prompts/:promptId',
* canAccessPromptResource({ requiredPermission: 1 }),
* getPrompt
* );
*
* @example
* // Custom resource ID parameter and edit permission
* router.patch('/prompts/:id',
* canAccessPromptResource({
* requiredPermission: 2,
* resourceIdParam: 'id'
* }),
* updatePrompt
* );
*/
const canAccessPromptResource = (options) => {
const { requiredPermission, resourceIdParam = 'promptId' } = options;
if (!requiredPermission || typeof requiredPermission !== 'number') {
throw new Error('canAccessPromptResource: requiredPermission is required and must be a number');
}
return canAccessResource({
resourceType: 'prompt',
requiredPermission,
resourceIdParam,
idResolver: resolvePromptId,
});
};
module.exports = {
canAccessPromptResource,
};

View file

@ -1,5 +1,6 @@
const { getPrompt } = require('~/models/Prompt');
const { ResourceType } = require('librechat-data-provider');
const { canAccessResource } = require('./canAccessResource');
const { getPrompt } = require('~/models/Prompt');
/**
* Prompt to PromptGroup ID resolver function
@ -42,7 +43,7 @@ const canAccessPromptViaGroup = (options) => {
}
return canAccessResource({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
requiredPermission,
resourceIdParam,
idResolver: resolvePromptToGroupId,

View file

@ -1,8 +1,8 @@
const { logger } = require('@librechat/data-schemas');
const { PERMISSION_BITS, hasPermissions } = require('librechat-data-provider');
const { PermissionBits, hasPermissions, ResourceType } = require('librechat-data-provider');
const { getEffectivePermissions } = require('~/server/services/PermissionService');
const { getFiles } = require('~/models/File');
const { getAgent } = require('~/models/Agent');
const { getFiles } = require('~/models/File');
/**
* Checks if user has access to a file through agent permissions
@ -35,11 +35,11 @@ const checkAgentBasedFileAccess = async (userId, fileId) => {
try {
const permissions = await getEffectivePermissions({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id || agent.id,
});
if (hasPermissions(permissions, PERMISSION_BITS.VIEW)) {
if (hasPermissions(permissions, PermissionBits.VIEW)) {
logger.debug(`[fileAccess] User ${userId} has VIEW permissions on agent ${agent.id}`);
return true;
}

View file

@ -1,7 +1,6 @@
const { canAccessResource } = require('./canAccessResource');
const { canAccessAgentResource } = require('./canAccessAgentResource');
const { canAccessAgentFromBody } = require('./canAccessAgentFromBody');
const { canAccessPromptResource } = require('./canAccessPromptResource');
const { canAccessPromptViaGroup } = require('./canAccessPromptViaGroup');
const { canAccessPromptGroupResource } = require('./canAccessPromptGroupResource');
@ -9,7 +8,6 @@ module.exports = {
canAccessResource,
canAccessAgentResource,
canAccessAgentFromBody,
canAccessPromptResource,
canAccessPromptViaGroup,
canAccessPromptGroupResource,
};

View file

@ -1,5 +1,5 @@
const express = require('express');
const { PermissionBits } = require('@librechat/data-schemas');
const { ResourceType, PermissionBits } = require('librechat-data-provider');
const {
getUserEffectivePermissions,
updateResourcePermissions,
@ -49,19 +49,17 @@ router.put(
// Use middleware that dynamically handles resource type and permissions
(req, res, next) => {
const { resourceType } = req.params;
// Define resource-specific middleware based on resourceType
let middleware;
if (resourceType === 'agent') {
if (resourceType === ResourceType.AGENT) {
middleware = canAccessResource({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermission: PermissionBits.SHARE,
resourceIdParam: 'resourceId',
});
} else if (resourceType === 'promptGroup') {
} else if (resourceType === ResourceType.PROMPTGROUP) {
middleware = canAccessResource({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
requiredPermission: PermissionBits.SHARE,
resourceIdParam: 'resourceId',
});

View file

@ -1,21 +1,22 @@
const express = require('express');
const { nanoid } = require('nanoid');
const { logger } = require('@librechat/data-schemas');
const { generateCheckAccess } = require('@librechat/api');
const { logger, PermissionBits } = require('@librechat/data-schemas');
const {
Permissions,
ResourceType,
PermissionTypes,
actionDelimiter,
PermissionBits,
removeNullishValues,
} = require('librechat-data-provider');
const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
const { findAccessibleResources } = require('~/server/services/PermissionService');
const { getAgent, updateAgent, getListAgentsByAccess } = require('~/models/Agent');
const { updateAction, getActions, deleteAction } = require('~/models/Action');
const { isActionDomainAllowed } = require('~/server/services/domains');
const { canAccessAgentResource } = require('~/server/middleware');
const { getAgent, updateAgent } = require('~/models/Agent');
const { getRoleByName } = require('~/models/Role');
const { getListAgentsByAccess } = require('~/models/Agent');
const router = express.Router();
@ -36,7 +37,7 @@ router.get('/', async (req, res) => {
const userId = req.user.id;
const editableAgentObjectIds = await findAccessibleResources({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: PermissionBits.EDIT,
});

View file

@ -1,7 +1,6 @@
const express = require('express');
const { PermissionBits } = require('@librechat/data-schemas');
const { generateCheckAccess, skipAgentCheck } = require('@librechat/api');
const { PermissionTypes, Permissions } = require('librechat-data-provider');
const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider');
const {
setHeaders,
moderateText,

View file

@ -1,7 +1,6 @@
const express = require('express');
const { generateCheckAccess } = require('@librechat/api');
const { PermissionBits } = require('@librechat/data-schemas');
const { PermissionTypes, Permissions } = require('librechat-data-provider');
const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider');
const { requireJwtAuth, canAccessAgentResource } = require('~/server/middleware');
const v1 = require('~/server/controllers/agents/v1');
const { getRoleByName } = require('~/models/Role');

View file

@ -4,6 +4,7 @@ const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { createMethods } = require('@librechat/data-schemas');
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
const { createAgent } = require('~/models/Agent');
const { createFile } = require('~/models/File');
@ -186,9 +187,9 @@ describe('File Routes - Agent Files Endpoint', () => {
await grantPermission({
principalType: 'user',
principalId: otherUserId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: authorId,
});
@ -241,9 +242,9 @@ describe('File Routes - Agent Files Endpoint', () => {
await grantPermission({
principalType: 'user',
principalId: otherUserId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: authorId,
});

View file

@ -6,8 +6,9 @@ const {
isUUID,
CacheKeys,
FileSources,
PERMISSION_BITS,
ResourceType,
EModelEndpoint,
PermissionBits,
isAgentsEndpoint,
checkOpenAIStorage,
} = require('librechat-data-provider');
@ -17,6 +18,7 @@ const {
processDeleteRequest,
processAgentFileUpload,
} = require('~/server/services/Files/process');
const { fileAccess } = require('~/server/middleware/accessResources/fileAccess');
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { checkPermission } = require('~/server/services/PermissionService');
@ -24,12 +26,11 @@ const { loadAuthValues } = require('~/server/services/Tools/credentials');
const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud');
const { hasAccessToFilesViaAgent } = require('~/server/services/Files');
const { getFiles, batchUpdateFiles } = require('~/models/File');
const { cleanFileName } = require('~/server/utils/files');
const { getAssistant } = require('~/models/Assistant');
const { getAgent } = require('~/models/Agent');
const { cleanFileName } = require('~/server/utils/files');
const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
const { fileAccess } = require('~/server/middleware/accessResources/fileAccess');
const router = express.Router();
@ -78,9 +79,9 @@ router.get('/agent/:agent_id', async (req, res) => {
if (agent.author.toString() !== userId) {
const hasEditPermission = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
requiredPermission: PERMISSION_BITS.EDIT,
requiredPermission: PermissionBits.EDIT,
});
if (!hasEditPermission) {

View file

@ -4,6 +4,7 @@ const mongoose = require('mongoose');
const { v4: uuidv4 } = require('uuid');
const { createMethods } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
const { createAgent } = require('~/models/Agent');
const { createFile } = require('~/models/File');
@ -228,9 +229,9 @@ describe('File Routes - Delete with Agent Access', () => {
await grantPermission({
principalType: 'user',
principalId: otherUserId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: authorId,
});
@ -282,9 +283,9 @@ describe('File Routes - Delete with Agent Access', () => {
await grantPermission({
principalType: 'user',
principalId: otherUserId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: authorId,
});
@ -348,9 +349,9 @@ describe('File Routes - Delete with Agent Access', () => {
await grantPermission({
principalType: 'user',
principalId: otherUserId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: authorId,
});
@ -391,9 +392,9 @@ describe('File Routes - Delete with Agent Access', () => {
await grantPermission({
principalType: 'user',
principalId: otherUserId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: authorId,
});

View file

@ -1,20 +1,26 @@
const express = require('express');
const { logger, PermissionBits } = require('@librechat/data-schemas');
const { logger } = require('@librechat/data-schemas');
const { generateCheckAccess } = require('@librechat/api');
const { Permissions, SystemRoles, PermissionTypes } = require('librechat-data-provider');
const {
getPrompt,
getPrompts,
savePrompt,
deletePrompt,
getPromptGroup,
getPromptGroups,
Permissions,
SystemRoles,
ResourceType,
AccessRoleIds,
PermissionTypes,
PermissionBits,
} = require('librechat-data-provider');
const {
makePromptProduction,
getAllPromptGroups,
updatePromptGroup,
deletePromptGroup,
createPromptGroup,
getAllPromptGroups,
// updatePromptLabels,
makePromptProduction,
getPromptGroups,
getPromptGroup,
deletePrompt,
getPrompts,
savePrompt,
getPrompt,
} = require('~/models/Prompt');
const {
canAccessPromptGroupResource,
@ -22,10 +28,10 @@ const {
requireJwtAuth,
} = require('~/server/middleware');
const {
grantPermission,
findPubliclyAccessibleResources,
getEffectivePermissions,
findAccessibleResources,
findPubliclyAccessibleResources,
grantPermission,
} = require('~/server/services/PermissionService');
const { getRoleByName } = require('~/models/Role');
@ -92,7 +98,7 @@ router.get('/all', async (req, res) => {
// Get promptGroup IDs the user has VIEW access to via ACL
const accessibleIds = await findAccessibleResources({
userId,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: PermissionBits.VIEW,
});
@ -123,13 +129,13 @@ router.get('/groups', async (req, res) => {
// Get promptGroup IDs the user has VIEW access to via ACL
const accessibleIds = await findAccessibleResources({
userId,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: PermissionBits.VIEW,
});
// Get publicly accessible promptGroups
const publiclyAccessibleIds = await findPubliclyAccessibleResources({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: PermissionBits.VIEW,
});
@ -185,9 +191,9 @@ const createNewPromptGroup = async (req, res) => {
await grantPermission({
principalType: 'user',
principalId: req.user.id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: result.prompt.groupId,
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: req.user.id,
});
logger.debug(
@ -327,7 +333,7 @@ router.get('/', async (req, res) => {
if (groupId) {
const permissions = await getEffectivePermissions({
userId: req.user.id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: groupId,
});

View file

@ -1,10 +1,14 @@
const request = require('supertest');
const express = require('express');
const { MongoMemoryServer } = require('mongodb-memory-server');
const request = require('supertest');
const mongoose = require('mongoose');
const { ObjectId } = require('mongodb');
const { SystemRoles } = require('librechat-data-provider');
const { PermissionBits } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const {
SystemRoles,
ResourceType,
AccessRoleIds,
PermissionBits,
} = require('librechat-data-provider');
// Mock modules before importing
jest.mock('~/server/services/Config', () => ({
@ -18,7 +22,6 @@ jest.mock('~/models/Role', () => ({
jest.mock('~/server/middleware', () => ({
requireJwtAuth: (req, res, next) => next(),
canAccessPromptResource: jest.requireActual('~/server/middleware').canAccessPromptResource,
canAccessPromptViaGroup: jest.requireActual('~/server/middleware').canAccessPromptViaGroup,
canAccessPromptGroupResource:
jest.requireActual('~/server/middleware').canAccessPromptGroupResource,
@ -90,21 +93,21 @@ async function setupTestData() {
// Create access roles for promptGroups
testRoles = {
viewer: await AccessRole.create({
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
name: 'Viewer',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits: PermissionBits.VIEW,
}),
editor: await AccessRole.create({
accessRoleId: 'promptGroup_editor',
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
name: 'Editor',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits: PermissionBits.VIEW | PermissionBits.EDIT,
}),
owner: await AccessRole.create({
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
name: 'Owner',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits:
PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE,
}),
@ -218,7 +221,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Check ACL entry was created
const aclEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: response.body.prompt.groupId,
principalType: 'user',
principalId: testUsers.owner._id,
@ -248,7 +251,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Check ACL entry was created for the promptGroup
const aclEntry = await AclEntry.findOne({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: response.body.group._id,
principalType: 'user',
principalId: testUsers.owner._id,
@ -293,9 +296,9 @@ describe('Prompt Routes - ACL Permissions', () => {
await grantPermission({
principalType: 'user',
principalId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
grantedBy: testUsers.owner._id,
});
@ -378,9 +381,9 @@ describe('Prompt Routes - ACL Permissions', () => {
await grantPermission({
principalType: 'user',
principalId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
accessRoleId: 'promptGroup_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: testUsers.owner._id,
});
});
@ -405,7 +408,7 @@ describe('Prompt Routes - ACL Permissions', () => {
// Verify ACL entries were removed
const aclEntries = await AclEntry.find({
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
});
expect(aclEntries).toHaveLength(0);
@ -425,9 +428,9 @@ describe('Prompt Routes - ACL Permissions', () => {
await grantPermission({
principalType: 'user',
principalId: testUsers.viewer._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
grantedBy: testUsers.editor._id,
});
@ -492,9 +495,9 @@ describe('Prompt Routes - ACL Permissions', () => {
await grantPermission({
principalType: 'user',
principalId: testUsers.owner._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
accessRoleId: 'promptGroup_editor',
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
grantedBy: testUsers.owner._id,
});
@ -530,9 +533,9 @@ describe('Prompt Routes - ACL Permissions', () => {
await grantPermission({
principalType: 'user',
principalId: testUsers.viewer._id,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
grantedBy: testUsers.owner._id,
});
@ -587,9 +590,9 @@ describe('Prompt Routes - ACL Permissions', () => {
await grantPermission({
principalType: 'public',
principalId: null,
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
resourceId: publicGroup._id,
accessRoleId: 'promptGroup_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
grantedBy: testUsers.owner._id,
});
});

View file

@ -1,5 +1,5 @@
const { logger } = require('@librechat/data-schemas');
const { PERMISSION_BITS } = require('librechat-data-provider');
const { PermissionBits, ResourceType } = require('librechat-data-provider');
const { checkPermission } = require('~/server/services/PermissionService');
const { getAgent } = require('~/models/Agent');
@ -32,9 +32,9 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
// Check if user has at least VIEW permission on the agent
const hasViewPermission = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
requiredPermission: PERMISSION_BITS.VIEW,
requiredPermission: PermissionBits.VIEW,
});
if (!hasViewPermission) {
@ -44,9 +44,9 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
// Check if user has EDIT permission (which would indicate collaborative access)
const hasEditPermission = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: agent._id,
requiredPermission: PERMISSION_BITS.EDIT,
requiredPermission: PermissionBits.EDIT,
});
// If user only has VIEW permission, they can't access files

View file

@ -1,32 +1,45 @@
const mongoose = require('mongoose');
const { isEnabled } = require('@librechat/api');
const { ResourceType } = require('librechat-data-provider');
const { getTransactionSupport, logger } = require('@librechat/data-schemas');
const { isEnabled } = require('~/server/utils');
const {
entraIdPrincipalFeatureEnabled,
getUserEntraGroups,
getUserOwnedEntraGroups,
getUserEntraGroups,
getGroupMembers,
getGroupOwners,
} = require('~/server/services/GraphApiService');
const {
findAccessibleResources: findAccessibleResourcesACL,
getEffectivePermissions: getEffectivePermissionsACL,
grantPermission: grantPermissionACL,
findEntriesByPrincipalsAndResource,
findGroupByExternalId,
findRoleByIdentifier,
getUserPrincipals,
hasPermission,
createGroup,
createUser,
updateUser,
findUser,
grantPermission: grantPermissionACL,
findAccessibleResources: findAccessibleResourcesACL,
hasPermission,
getEffectivePermissions: getEffectivePermissionsACL,
findEntriesByPrincipalsAndResource,
} = require('~/models');
const { AclEntry, AccessRole, Group } = require('~/db/models');
/** @type {boolean|null} */
let transactionSupportCache = null;
/**
* Validates that the resourceType is one of the supported enum values
* @param {string} resourceType - The resource type to validate
* @throws {Error} If resourceType is not valid
*/
const validateResourceType = (resourceType) => {
const validTypes = Object.values(ResourceType);
if (!validTypes.includes(resourceType)) {
throw new Error(`Invalid resourceType: ${resourceType}. Valid types: ${validTypes.join(', ')}`);
}
};
/**
* @import { TPrincipal } from 'librechat-data-provider'
*/
@ -37,7 +50,7 @@ let transactionSupportCache = null;
* @param {string|mongoose.Types.ObjectId|null} params.principalId - The ID of the principal (null for 'public')
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
* @param {string} params.accessRoleId - The ID of the role (e.g., 'agent_viewer', 'agent_editor')
* @param {string} params.accessRoleId - The ID of the role (e.g., AccessRoleIds.AGENT_VIEWER, AccessRoleIds.AGENT_EDITOR)
* @param {string|mongoose.Types.ObjectId} params.grantedBy - User ID granting the permission
* @param {mongoose.ClientSession} [params.session] - Optional MongoDB session for transactions
* @returns {Promise<Object>} The created or updated ACL entry
@ -68,6 +81,8 @@ const grantPermission = async ({
throw new Error(`Invalid resource ID: ${resourceId}`);
}
validateResourceType(resourceType);
// Get the role to determine permission bits
const role = await findRoleByIdentifier(accessRoleId);
if (!role) {
@ -111,6 +126,8 @@ const checkPermission = async ({ userId, resourceType, resourceId, requiredPermi
throw new Error('requiredPermission must be a positive number');
}
validateResourceType(resourceType);
// Get all principals for the user (user + groups + public)
const principals = await getUserPrincipals(userId);
@ -139,6 +156,8 @@ const checkPermission = async ({ userId, resourceType, resourceId, requiredPermi
*/
const getEffectivePermissions = async ({ userId, resourceType, resourceId }) => {
try {
validateResourceType(resourceType);
// Get all principals for the user (user + groups + public)
const principals = await getUserPrincipals(userId);
@ -166,6 +185,8 @@ const findAccessibleResources = async ({ userId, resourceType, requiredPermissio
throw new Error('requiredPermissions must be a positive number');
}
validateResourceType(resourceType);
// Get all principals for the user (user + groups + public)
const principalsList = await getUserPrincipals(userId);
@ -196,6 +217,8 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio
throw new Error('requiredPermissions must be a positive number');
}
validateResourceType(resourceType);
// Find all public ACL entries where the public principal has at least the required permission bits
const entries = await AclEntry.find({
principalType: 'public',
@ -221,12 +244,9 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio
* @returns {Promise<Array>} Array of role definitions
*/
const getAvailableRoles = async ({ resourceType }) => {
try {
return await AccessRole.find({ resourceType }).lean();
} catch (error) {
logger.error(`[PermissionService.getAvailableRoles] Error: ${error.message}`);
return [];
}
validateResourceType(resourceType);
return await AccessRole.find({ resourceType }).lean();
};
/**
@ -482,6 +502,8 @@ const hasPublicPermission = async ({ resourceType, resourceId, requiredPermissio
throw new Error('requiredPermissions must be a positive number');
}
validateResourceType(resourceType);
// Use public principal to check permissions
const publicPrincipal = [{ principalType: 'public' }];
@ -707,14 +729,16 @@ const bulkUpdateResourcePermissions = async ({
};
/**
* Remove all permissions for a specific resource
* @param {Object} params - Parameters for removing permissions
* Remove all permissions for a resource (cleanup when resource is deleted)
* @param {Object} params - Parameters for removing all permissions
* @param {string} params.resourceType - Type of resource (e.g., 'agent', 'prompt')
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
* @returns {Promise<Object>} Delete result
* @returns {Promise<Object>} Result of the deletion operation
*/
const removeAllPermissions = async ({ resourceType, resourceId }) => {
try {
validateResourceType(resourceType);
if (!resourceId || !mongoose.Types.ObjectId.isValid(resourceId)) {
throw new Error(`Invalid resource ID: ${resourceId}`);
}

View file

@ -1,6 +1,7 @@
const mongoose = require('mongoose');
const { RoleBits } = require('@librechat/data-schemas');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
const {
bulkUpdateResourcePermissions,
getEffectivePermissions,
@ -48,49 +49,49 @@ beforeEach(async () => {
// Seed some roles for testing
await AccessRole.create([
{
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Agent Viewer',
description: 'Can view agents',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, // VIEW permission
},
{
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'Agent Editor',
description: 'Can edit agents',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR, // VIEW + EDIT permissions
},
{
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
name: 'Agent Owner',
description: 'Full control over agents',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.OWNER, // VIEW + EDIT + DELETE + SHARE permissions
},
{
accessRoleId: 'project_viewer',
accessRoleId: AccessRoleIds.PROJECT_VIEWER,
name: 'Project Viewer',
description: 'Can view projects',
resourceType: 'project',
permBits: RoleBits.VIEWER,
},
{
accessRoleId: 'project_editor',
accessRoleId: AccessRoleIds.PROJECT_EDITOR,
name: 'Project Editor',
description: 'Can edit projects',
resourceType: 'project',
permBits: RoleBits.EDITOR,
},
{
accessRoleId: 'project_manager',
accessRoleId: AccessRoleIds.PROJECT_MANAGER,
name: 'Project Manager',
description: 'Can manage projects',
resourceType: 'project',
permBits: RoleBits.MANAGER,
},
{
accessRoleId: 'project_owner',
accessRoleId: AccessRoleIds.PROJECT_OWNER,
name: 'Project Owner',
description: 'Full control over projects',
resourceType: 'project',
@ -117,9 +118,9 @@ describe('PermissionService', () => {
const entry = await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
@ -131,7 +132,7 @@ describe('PermissionService', () => {
expect(entry.resourceId.toString()).toBe(resourceId.toString());
// Get the role to verify the permission bits are correctly set
const role = await findRoleByIdentifier('agent_viewer');
const role = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(entry.permBits).toBe(role.permBits);
expect(entry.roleId.toString()).toBe(role._id.toString());
expect(entry.grantedBy.toString()).toBe(grantedById.toString());
@ -142,9 +143,9 @@ describe('PermissionService', () => {
const entry = await grantPermission({
principalType: 'group',
principalId: groupId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
@ -154,7 +155,7 @@ describe('PermissionService', () => {
expect(entry.principalModel).toBe('Group');
// Get the role to verify the permission bits are correctly set
const role = await findRoleByIdentifier('agent_editor');
const role = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
expect(entry.permBits).toBe(role.permBits);
expect(entry.roleId.toString()).toBe(role._id.toString());
});
@ -163,9 +164,9 @@ describe('PermissionService', () => {
const entry = await grantPermission({
principalType: 'public',
principalId: null,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
@ -175,7 +176,7 @@ describe('PermissionService', () => {
expect(entry.principalModel).toBeUndefined();
// Get the role to verify the permission bits are correctly set
const role = await findRoleByIdentifier('agent_viewer');
const role = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(entry.permBits).toBe(role.permBits);
expect(entry.roleId.toString()).toBe(role._id.toString());
});
@ -185,9 +186,9 @@ describe('PermissionService', () => {
grantPermission({
principalType: 'invalid',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Invalid principal type: invalid');
@ -198,9 +199,9 @@ describe('PermissionService', () => {
grantPermission({
principalType: 'user',
principalId: null,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
}),
).rejects.toThrow('Principal ID is required for user and group principals');
@ -211,7 +212,7 @@ describe('PermissionService', () => {
grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'non_existent_role',
grantedBy: grantedById,
@ -224,9 +225,9 @@ describe('PermissionService', () => {
grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'project_viewer', // Project role for agent resource
accessRoleId: AccessRoleIds.PROJECT_VIEWER, // Project role for agent resource
grantedBy: grantedById,
}),
).rejects.toThrow('Role project_viewer is for project resources, not agent');
@ -237,9 +238,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
@ -247,13 +248,13 @@ describe('PermissionService', () => {
const updated = await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
const editorRole = await findRoleByIdentifier('agent_editor');
const editorRole = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
expect(updated.permBits).toBe(editorRole.permBits);
expect(updated.roleId.toString()).toBe(editorRole._id.toString());
@ -261,7 +262,7 @@ describe('PermissionService', () => {
const entries = await AclEntry.find({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
});
expect(entries).toHaveLength(1);
@ -279,9 +280,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
@ -289,9 +290,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'group',
principalId: groupId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: otherResourceId,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
});
@ -302,7 +303,7 @@ describe('PermissionService', () => {
const hasViewPermission = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW
});
@ -312,7 +313,7 @@ describe('PermissionService', () => {
// Check higher permission level that user doesn't have
const hasEditPermission = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
requiredPermission: 3, // RoleBits.EDITOR = VIEW + EDIT
});
@ -330,7 +331,7 @@ describe('PermissionService', () => {
// Check original resource (user has access)
const hasViewOnOriginal = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW
});
@ -340,7 +341,7 @@ describe('PermissionService', () => {
// Check other resource (group has access)
const hasViewOnOther = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: otherResourceId,
requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW
});
@ -356,9 +357,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'public',
principalId: null,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: publicResourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
@ -371,7 +372,7 @@ describe('PermissionService', () => {
const hasPublicAccess = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: publicResourceId,
requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW
});
@ -385,7 +386,7 @@ describe('PermissionService', () => {
await expect(
checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
requiredPermission: 'invalid',
}),
@ -393,7 +394,7 @@ describe('PermissionService', () => {
const nonExistentResource = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: new mongoose.Types.ObjectId(),
requiredPermission: 1, // RoleBits.VIEWER
});
@ -406,7 +407,7 @@ describe('PermissionService', () => {
const hasPermission = await checkPermission({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
requiredPermission: 1, // RoleBits.VIEWER
});
@ -424,18 +425,18 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
await grantPermission({
principalType: 'group',
principalId: groupId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
@ -444,9 +445,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'public',
principalId: null,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: publicResourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
@ -459,7 +460,7 @@ describe('PermissionService', () => {
principalId: userId,
resourceType: 'project',
resourceId: projectId,
accessRoleId: 'project_viewer',
accessRoleId: AccessRoleIds.PROJECT_VIEWER,
grantedBy: grantedById,
});
@ -467,10 +468,10 @@ describe('PermissionService', () => {
principalType: 'user',
principalId: userId,
principalModel: 'User',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: childResourceId,
permBits: RoleBits.VIEWER,
roleId: (await findRoleByIdentifier('agent_viewer'))._id,
roleId: (await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER))._id,
grantedBy: grantedById,
grantedAt: new Date(),
inheritedFrom: projectId,
@ -486,7 +487,7 @@ describe('PermissionService', () => {
const effective = await getEffectivePermissions({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
});
@ -505,7 +506,7 @@ describe('PermissionService', () => {
const effective = await getEffectivePermissions({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: childResourceId,
});
@ -519,7 +520,7 @@ describe('PermissionService', () => {
const nonExistentResource = new mongoose.Types.ObjectId();
const effective = await getEffectivePermissions({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: nonExistentResource,
});
@ -532,7 +533,7 @@ describe('PermissionService', () => {
const effective = await getEffectivePermissions({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
});
@ -555,9 +556,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: resource1,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
@ -565,9 +566,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: resource2,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
@ -575,9 +576,9 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'group',
principalId: groupId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: resource3,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
});
@ -588,7 +589,7 @@ describe('PermissionService', () => {
const viewableResources = await findAccessibleResources({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: 1, // RoleBits.VIEWER // 1 = VIEW
});
@ -602,7 +603,7 @@ describe('PermissionService', () => {
const editableResources = await findAccessibleResources({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: 3, // RoleBits.EDITOR = VIEW + EDIT
});
@ -619,7 +620,7 @@ describe('PermissionService', () => {
const viewableResources = await findAccessibleResources({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: 1, // RoleBits.VIEWER // 1 = VIEW
});
@ -633,7 +634,7 @@ describe('PermissionService', () => {
await expect(
findAccessibleResources({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: 'invalid',
}),
).rejects.toThrow('requiredPermissions must be a positive number');
@ -652,7 +653,7 @@ describe('PermissionService', () => {
const resources = await findAccessibleResources({
userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
requiredPermissions: 1, // RoleBits.VIEWER
});
@ -663,12 +664,12 @@ describe('PermissionService', () => {
describe('getAvailableRoles', () => {
test('should get all roles for a resource type', async () => {
const roles = await getAvailableRoles({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
});
expect(roles).toHaveLength(3);
expect(roles.map((r) => r.accessRoleId).sort()).toEqual(
['agent_editor', 'agent_owner', 'agent_viewer'].sort(),
[AccessRoleIds.AGENT_EDITOR, AccessRoleIds.AGENT_OWNER, AccessRoleIds.AGENT_VIEWER].sort(),
);
});
@ -689,27 +690,27 @@ describe('PermissionService', () => {
await grantPermission({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
await grantPermission({
principalType: 'group',
principalId: groupId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
grantedBy: grantedById,
});
await grantPermission({
principalType: 'public',
principalId: null,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
grantedBy: grantedById,
});
});
@ -720,22 +721,22 @@ describe('PermissionService', () => {
{
type: 'user',
id: userId,
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
},
{
type: 'user',
id: otherUserId,
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
},
{
type: 'group',
id: groupId,
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
},
];
const results = await bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: newResourceId,
updatedPrincipals,
grantedBy: grantedById,
@ -748,7 +749,7 @@ describe('PermissionService', () => {
// Verify permissions were created
const aclEntries = await AclEntry.find({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: newResourceId,
});
expect(aclEntries).toHaveLength(3);
@ -759,21 +760,21 @@ describe('PermissionService', () => {
{
type: 'user',
id: userId,
accessRoleId: 'agent_editor', // Upgrade from viewer to editor
accessRoleId: AccessRoleIds.AGENT_EDITOR, // Upgrade from viewer to editor
},
{
type: 'group',
id: groupId,
accessRoleId: 'agent_owner', // Upgrade from editor to owner
accessRoleId: AccessRoleIds.AGENT_OWNER, // Upgrade from editor to owner
},
{
type: 'public',
accessRoleId: 'agent_viewer', // Keep same role
accessRoleId: AccessRoleIds.AGENT_VIEWER, // Keep same role
},
];
const results = await bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
updatedPrincipals,
grantedBy: grantedById,
@ -789,18 +790,18 @@ describe('PermissionService', () => {
const userEntry = await AclEntry.findOne({
principalType: 'user',
principalId: userId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
}).populate('roleId', 'accessRoleId');
expect(userEntry.roleId.accessRoleId).toBe('agent_editor');
expect(userEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
const groupEntry = await AclEntry.findOne({
principalType: 'group',
principalId: groupId,
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
}).populate('roleId', 'accessRoleId');
expect(groupEntry.roleId.accessRoleId).toBe('agent_owner');
expect(groupEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_OWNER);
});
test('should revoke specified permissions', async () => {
@ -815,7 +816,7 @@ describe('PermissionService', () => {
];
const results = await bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
revokedPrincipals,
grantedBy: grantedById,
@ -828,7 +829,7 @@ describe('PermissionService', () => {
// Verify only user permission remains
const remainingEntries = await AclEntry.find({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
});
expect(remainingEntries).toHaveLength(1);
@ -841,12 +842,12 @@ describe('PermissionService', () => {
{
type: 'user',
id: userId,
accessRoleId: 'agent_owner', // Update existing
accessRoleId: AccessRoleIds.AGENT_OWNER, // Update existing
},
{
type: 'user',
id: otherUserId,
accessRoleId: 'agent_viewer', // New permission
accessRoleId: AccessRoleIds.AGENT_VIEWER, // New permission
},
];
@ -861,7 +862,7 @@ describe('PermissionService', () => {
];
const results = await bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
updatedPrincipals,
revokedPrincipals,
@ -875,19 +876,19 @@ describe('PermissionService', () => {
// Verify final state
const finalEntries = await AclEntry.find({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
}).populate('roleId', 'accessRoleId');
expect(finalEntries).toHaveLength(2);
const userEntry = finalEntries.find((e) => e.principalId.toString() === userId.toString());
expect(userEntry.roleId.accessRoleId).toBe('agent_owner');
expect(userEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_OWNER);
const otherUserEntry = finalEntries.find(
(e) => e.principalId.toString() === otherUserId.toString(),
);
expect(otherUserEntry.roleId.accessRoleId).toBe('agent_viewer');
expect(otherUserEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
});
test('should handle errors for invalid roles gracefully', async () => {
@ -895,7 +896,7 @@ describe('PermissionService', () => {
{
type: 'user',
id: userId,
accessRoleId: 'agent_viewer', // Valid
accessRoleId: AccessRoleIds.AGENT_VIEWER, // Valid
},
{
type: 'user',
@ -905,12 +906,12 @@ describe('PermissionService', () => {
{
type: 'group',
id: groupId,
accessRoleId: 'project_viewer', // Wrong resource type
accessRoleId: AccessRoleIds.PROJECT_VIEWER, // Wrong resource type
},
];
const results = await bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
updatedPrincipals,
grantedBy: grantedById,
@ -928,7 +929,7 @@ describe('PermissionService', () => {
test('should handle empty arrays (no operations)', async () => {
const results = await bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
updatedPrincipals: [],
revokedPrincipals: [],
@ -942,7 +943,7 @@ describe('PermissionService', () => {
// Verify no changes to existing permissions (since no operations were performed)
const remainingEntries = await AclEntry.find({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
});
expect(remainingEntries).toHaveLength(3); // Original permissions still exist
@ -951,7 +952,7 @@ describe('PermissionService', () => {
test('should throw error for invalid updatedPrincipals array', async () => {
await expect(
bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
updatedPrincipals: 'not an array',
grantedBy: grantedById,
@ -962,7 +963,7 @@ describe('PermissionService', () => {
test('should throw error for invalid resource ID', async () => {
await expect(
bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: 'invalid-id',
permissions: [],
grantedBy: grantedById,
@ -974,12 +975,12 @@ describe('PermissionService', () => {
const updatedPrincipals = [
{
type: 'public',
accessRoleId: 'agent_editor', // Update public permission
accessRoleId: AccessRoleIds.AGENT_EDITOR, // Update public permission
},
{
type: 'user',
id: otherUserId,
accessRoleId: 'agent_viewer', // New user permission
accessRoleId: AccessRoleIds.AGENT_VIEWER, // New user permission
},
];
@ -995,7 +996,7 @@ describe('PermissionService', () => {
];
const results = await bulkUpdateResourcePermissions({
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
updatedPrincipals,
revokedPrincipals,
@ -1010,12 +1011,12 @@ describe('PermissionService', () => {
// Verify public permission was updated
const publicEntry = await AclEntry.findOne({
principalType: 'public',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId,
}).populate('roleId', 'accessRoleId');
expect(publicEntry).toBeDefined();
expect(publicEntry.roleId.accessRoleId).toBe('agent_editor');
expect(publicEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
});
test('should work with different resource types', async () => {
@ -1025,12 +1026,12 @@ describe('PermissionService', () => {
{
type: 'user',
id: userId,
accessRoleId: 'project_viewer',
accessRoleId: AccessRoleIds.PROJECT_VIEWER,
},
{
type: 'group',
id: groupId,
accessRoleId: 'project_editor',
accessRoleId: AccessRoleIds.PROJECT_EDITOR,
},
];

View file

@ -1072,6 +1072,19 @@
* @memberof typedefs
*/
/** Permissions */
/**
* @exports TUpdateResourcePermissionsRequest
* @typedef {import('librechat-data-provider').TUpdateResourcePermissionsRequest} TUpdateResourcePermissionsRequest
* @memberof typedefs
*/
/**
* @exports TUpdateResourcePermissionsResponse
* @typedef {import('librechat-data-provider').TUpdateResourcePermissionsResponse} TUpdateResourcePermissionsResponse
* @memberof typedefs
*/
/**
* @exports JsonSchemaType
* @typedef {import('@librechat/api').JsonSchemaType} JsonSchemaType