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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,12 +4,13 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { ResourceType } = require('librechat-data-provider');
const { const {
getAvailableRoles,
ensurePrincipalExists,
getEffectivePermissions,
ensureGroupPrincipalExists,
bulkUpdateResourcePermissions, bulkUpdateResourcePermissions,
ensureGroupPrincipalExists,
getEffectivePermissions,
ensurePrincipalExists,
getAvailableRoles,
} = require('~/server/services/PermissionService'); } = require('~/server/services/PermissionService');
const { AclEntry } = require('~/db/models'); const { AclEntry } = require('~/db/models');
const { const {
@ -18,8 +19,8 @@ const {
calculateRelevanceScore, calculateRelevanceScore,
} = require('~/models'); } = require('~/models');
const { const {
searchEntraIdPrincipals,
entraIdPrincipalFeatureEnabled, entraIdPrincipalFeatureEnabled,
searchEntraIdPrincipals,
} = require('~/server/services/GraphApiService'); } = require('~/server/services/GraphApiService');
/** /**
@ -27,6 +28,18 @@ const {
* Delegates validation and logic to PermissionService * 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) * Bulk update permissions for a resource (grant, update, remove)
* @route PUT /api/{resourceType}/{resourceId}/permissions * @route PUT /api/{resourceType}/{resourceId}/permissions
@ -41,6 +54,8 @@ const {
const updateResourcePermissions = async (req, res) => { const updateResourcePermissions = async (req, res) => {
try { try {
const { resourceType, resourceId } = req.params; const { resourceType, resourceId } = req.params;
validateResourceType(resourceType);
/** @type {TUpdateResourcePermissionsRequest} */ /** @type {TUpdateResourcePermissionsRequest} */
const { updated, removed, public: isPublic, publicAccessRoleId } = req.body; const { updated, removed, public: isPublic, publicAccessRoleId } = req.body;
const { id: userId } = req.user; const { id: userId } = req.user;
@ -163,6 +178,7 @@ const updateResourcePermissions = async (req, res) => {
const getResourcePermissions = async (req, res) => { const getResourcePermissions = async (req, res) => {
try { try {
const { resourceType, resourceId } = req.params; const { resourceType, resourceId } = req.params;
validateResourceType(resourceType);
// Use aggregation pipeline for efficient single-query data retrieval // Use aggregation pipeline for efficient single-query data retrieval
const results = await AclEntry.aggregate([ const results = await AclEntry.aggregate([
@ -278,6 +294,7 @@ const getResourcePermissions = async (req, res) => {
const getResourceRoles = async (req, res) => { const getResourceRoles = async (req, res) => {
try { try {
const { resourceType } = req.params; const { resourceType } = req.params;
validateResourceType(resourceType);
const roles = await getAvailableRoles({ resourceType }); const roles = await getAvailableRoles({ resourceType });
@ -305,6 +322,8 @@ const getResourceRoles = async (req, res) => {
const getUserEffectivePermissions = async (req, res) => { const getUserEffectivePermissions = async (req, res) => {
try { try {
const { resourceType, resourceId } = req.params; const { resourceType, resourceId } = req.params;
validateResourceType(resourceType);
const { id: userId } = req.user; const { id: userId } = req.user;
const permissionBits = await getEffectivePermissions({ const permissionBits = await getEffectivePermissions({

View file

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

View file

@ -1,5 +1,5 @@
const { logger } = require('@librechat/data-schemas'); 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 { canAccessResource } = require('./canAccessResource');
const { getAgent } = require('~/models/Agent'); const { getAgent } = require('~/models/Agent');
@ -67,7 +67,7 @@ const canAccessAgentFromBody = (options) => {
} }
const agentAccessMiddleware = canAccessResource({ const agentAccessMiddleware = canAccessResource({
resourceType: 'agent', resourceType: ResourceType.AGENT,
requiredPermission, requiredPermission,
resourceIdParam: 'agent_id', // This will be ignored since we use custom resolver resourceIdParam: 'agent_id', // This will be ignored since we use custom resolver
idResolver: () => resolveAgentIdFromBody(agentId), 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 { canAccessResource } = require('./canAccessResource');
const { getAgent } = require('~/models/Agent');
/** /**
* Agent ID resolver function * Agent ID resolver function
@ -46,7 +47,7 @@ const canAccessAgentResource = (options) => {
} }
return canAccessResource({ return canAccessResource({
resourceType: 'agent', resourceType: ResourceType.AGENT,
requiredPermission, requiredPermission,
resourceIdParam, resourceIdParam,
idResolver: resolveAgentId, idResolver: resolveAgentId,

View file

@ -1,4 +1,5 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { ResourceType } = 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 +100,7 @@ describe('canAccessAgentResource middleware', () => {
principalType: 'user', principalType: 'user',
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: 'User',
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)
grantedBy: testUser._id, grantedBy: testUser._id,
@ -136,7 +137,7 @@ describe('canAccessAgentResource middleware', () => {
principalType: 'user', principalType: 'user',
principalId: otherUser._id, principalId: otherUser._id,
principalModel: 'User', principalModel: 'User',
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions permBits: 15, // All permissions
grantedBy: otherUser._id, grantedBy: otherUser._id,
@ -177,7 +178,7 @@ describe('canAccessAgentResource middleware', () => {
principalType: 'user', principalType: 'user',
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: 'User',
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 1, // VIEW permission permBits: 1, // VIEW permission
grantedBy: otherUser._id, grantedBy: otherUser._id,
@ -214,7 +215,7 @@ describe('canAccessAgentResource middleware', () => {
principalType: 'user', principalType: 'user',
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: 'User',
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 1, // VIEW permission only permBits: 1, // VIEW permission only
grantedBy: otherUser._id, grantedBy: otherUser._id,
@ -261,7 +262,7 @@ describe('canAccessAgentResource middleware', () => {
principalType: 'user', principalType: 'user',
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: 'User',
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions permBits: 15, // All permissions
grantedBy: testUser._id, grantedBy: testUser._id,
@ -297,7 +298,7 @@ describe('canAccessAgentResource middleware', () => {
principalType: 'user', principalType: 'user',
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: 'User',
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)
grantedBy: testUser._id, grantedBy: testUser._id,
@ -357,7 +358,7 @@ describe('canAccessAgentResource middleware', () => {
principalType: 'user', principalType: 'user',
principalId: testUser._id, principalId: testUser._id,
principalModel: 'User', principalModel: 'User',
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
permBits: 15, // All permissions permBits: 15, // All permissions
grantedBy: testUser._id, 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 { canAccessResource } = require('./canAccessResource');
const { getPromptGroup } = require('~/models/Prompt');
/** /**
* PromptGroup ID resolver function * PromptGroup ID resolver function
@ -48,7 +49,7 @@ const canAccessPromptGroupResource = (options) => {
} }
return canAccessResource({ return canAccessResource({
resourceType: 'promptGroup', resourceType: ResourceType.PROMPTGROUP,
requiredPermission, requiredPermission,
resourceIdParam, resourceIdParam,
idResolver: resolvePromptGroupId, 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 { canAccessResource } = require('./canAccessResource');
const { getPrompt } = require('~/models/Prompt');
/** /**
* Prompt to PromptGroup ID resolver function * Prompt to PromptGroup ID resolver function
@ -42,7 +43,7 @@ const canAccessPromptViaGroup = (options) => {
} }
return canAccessResource({ return canAccessResource({
resourceType: 'promptGroup', resourceType: ResourceType.PROMPTGROUP,
requiredPermission, requiredPermission,
resourceIdParam, resourceIdParam,
idResolver: resolvePromptToGroupId, idResolver: resolvePromptToGroupId,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1072,6 +1072,19 @@
* @memberof typedefs * @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 * @exports JsonSchemaType
* @typedef {import('@librechat/api').JsonSchemaType} JsonSchemaType * @typedef {import('@librechat/api').JsonSchemaType} JsonSchemaType

View file

@ -1,10 +1,7 @@
import React from 'react'; import React from 'react';
import type t from 'librechat-data-provider'; import type t from 'librechat-data-provider';
import useLocalize from '~/hooks/useLocalize'; import useLocalize from '~/hooks/useLocalize';
import { renderAgentAvatar, getContactDisplayName } from '~/utils/agents'; import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils';
import { cn } from '~/utils';
interface AgentCardProps { interface AgentCardProps {
agent: t.Agent; // The agent data to display agent: t.Agent; // The agent data to display

View file

@ -6,7 +6,7 @@ import {
QueryKeys, QueryKeys,
Constants, Constants,
EModelEndpoint, EModelEndpoint,
PERMISSION_BITS, PermissionBits,
LocalStorageKeys, LocalStorageKeys,
AgentListResponse, AgentListResponse,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
@ -45,7 +45,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
*/ */
const handleStartChat = () => { const handleStartChat = () => {
if (agent) { if (agent) {
const keys = [QueryKeys.agents, { requiredPermission: PERMISSION_BITS.EDIT }]; const keys = [QueryKeys.agents, { requiredPermission: PermissionBits.EDIT }];
const listResp = queryClient.getQueryData<AgentListResponse>(keys); const listResp = queryClient.getQueryData<AgentListResponse>(keys);
if (listResp != null) { if (listResp != null) {
if (!listResp.data.some((a) => a.id === agent.id)) { if (!listResp.data.some((a) => a.id === agent.id)) {

View file

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Button, Spinner } from '@librechat/client'; import { Button, Spinner } from '@librechat/client';
import { PERMISSION_BITS } from 'librechat-data-provider'; import { PermissionBits } from 'librechat-data-provider';
import type t from 'librechat-data-provider'; import type t from 'librechat-data-provider';
import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents';
import { useAgentCategories, useLocalize } from '~/hooks'; import { useAgentCategories, useLocalize } from '~/hooks';
@ -33,7 +33,7 @@ const AgentGrid: React.FC<AgentGridProps> = ({ category, searchQuery, onSelectAg
limit: number; limit: number;
promoted?: 0 | 1; promoted?: 0 | 1;
} = { } = {
requiredPermission: PERMISSION_BITS.VIEW, // View permission for marketplace viewing requiredPermission: PermissionBits.VIEW, // View permission for marketplace viewing
limit: 6, limit: 6,
}; };

View file

@ -7,7 +7,7 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@librechat/client'; } from '@librechat/client';
import { PERMISSION_BITS } from 'librechat-data-provider'; import { PermissionBits } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider';
import { useLocalize, useSubmitMessage, useCustomLink, useResourcePermissions } from '~/hooks'; import { useLocalize, useSubmitMessage, useCustomLink, useResourcePermissions } from '~/hooks';
import VariableDialog from '~/components/Prompts/Groups/VariableDialog'; import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
@ -35,7 +35,7 @@ function ChatGroupItem({
// Check permissions for the promptGroup // Check permissions for the promptGroup
const { hasPermission } = useResourcePermissions('promptGroup', group._id || ''); const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
const canEdit = hasPermission(PERMISSION_BITS.EDIT); const canEdit = hasPermission(PermissionBits.EDIT);
const onCardClick: React.MouseEventHandler<HTMLButtonElement> = () => { const onCardClick: React.MouseEventHandler<HTMLButtonElement> = () => {
const text = group.productionPrompt?.prompt; const text = group.productionPrompt?.prompt;

View file

@ -1,7 +1,7 @@
import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react'; import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react';
import { EarthIcon, Pen } from 'lucide-react'; import { EarthIcon, Pen } from 'lucide-react';
import { useNavigate, useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { PERMISSION_BITS, type TPromptGroup } from 'librechat-data-provider'; import { PermissionBits, type TPromptGroup } from 'librechat-data-provider';
import { import {
Input, Input,
Label, Label,
@ -30,8 +30,8 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps
const [nameInputValue, setNameInputValue] = useState(group.name); const [nameInputValue, setNameInputValue] = useState(group.name);
const { hasPermission } = useResourcePermissions('promptGroup', group._id || ''); const { hasPermission } = useResourcePermissions('promptGroup', group._id || '');
const canEdit = hasPermission(PERMISSION_BITS.EDIT); const canEdit = hasPermission(PermissionBits.EDIT);
const canDelete = hasPermission(PERMISSION_BITS.DELETE); const canDelete = hasPermission(PermissionBits.DELETE);
const isGlobalGroup = useMemo( const isGlobalGroup = useMemo(
() => instanceProjectId && group.projectIds?.includes(instanceProjectId), () => instanceProjectId && group.projectIds?.includes(instanceProjectId),

View file

@ -6,7 +6,7 @@ import { Menu, Rocket } from 'lucide-react';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import { useParams, useOutletContext } from 'react-router-dom'; import { useParams, useOutletContext } from 'react-router-dom';
import { Button, Skeleton, useToastContext } from '@librechat/client'; import { Button, Skeleton, useToastContext } from '@librechat/client';
import { Permissions, PermissionTypes, PERMISSION_BITS } from 'librechat-data-provider'; import { Permissions, PermissionTypes, PermissionBits } from 'librechat-data-provider';
import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider'; import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider';
import { import {
useGetPrompts, useGetPrompts,
@ -186,8 +186,8 @@ const PromptForm = () => {
group?._id || '', group?._id || '',
); );
const canEdit = hasPermission(PERMISSION_BITS.EDIT); const canEdit = hasPermission(PermissionBits.EDIT);
const canView = hasPermission(PERMISSION_BITS.VIEW); const canView = hasPermission(PermissionBits.VIEW);
const methods = useForm({ const methods = useForm({
defaultValues: { defaultValues: {

View file

@ -3,8 +3,9 @@ import { Share2Icon } from 'lucide-react';
import { import {
SystemRoles, SystemRoles,
Permissions, Permissions,
ResourceType,
PermissionTypes, PermissionTypes,
PERMISSION_BITS, PermissionBits,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import { Button } from '@librechat/client'; import { Button } from '@librechat/client';
import type { TPromptGroup } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider';
@ -25,7 +26,7 @@ const SharePrompt = React.memo(
// The query will be disabled if groupId is empty // The query will be disabled if groupId is empty
const groupId = group?._id || ''; const groupId = group?._id || '';
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions( const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(
'promptGroup', ResourceType.PROMPTGROUP,
groupId, groupId,
); );
@ -34,7 +35,7 @@ const SharePrompt = React.memo(
return null; return null;
} }
const canShareThisPrompt = hasPermission(PERMISSION_BITS.SHARE); const canShareThisPrompt = hasPermission(PermissionBits.SHARE);
const shouldShowShareButton = const shouldShowShareButton =
(group.author === user?.id || user?.role === SystemRoles.ADMIN || canShareThisPrompt) && (group.author === user?.id || user?.role === SystemRoles.ADMIN || canShareThisPrompt) &&
@ -49,7 +50,7 @@ const SharePrompt = React.memo(
<GenericGrantAccessDialog <GenericGrantAccessDialog
resourceDbId={groupId} resourceDbId={groupId}
resourceName={group.name} resourceName={group.name}
resourceType="promptGroup" resourceType={ResourceType.PROMPTGROUP}
disabled={disabled} disabled={disabled}
> >
<Button <Button

View file

@ -2,7 +2,7 @@ import React from 'react';
import * as Ariakit from '@ariakit/react'; import * as Ariakit from '@ariakit/react';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { DropdownPopup } from '@librechat/client'; import { DropdownPopup } from '@librechat/client';
import { ACCESS_ROLE_IDS } from 'librechat-data-provider'; import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { useGetAccessRolesQuery } from 'librechat-data-provider/react-query'; import { useGetAccessRolesQuery } from 'librechat-data-provider/react-query';
import type { AccessRole } from 'librechat-data-provider'; import type { AccessRole } from 'librechat-data-provider';
import type * as t from '~/common'; import type * as t from '~/common';
@ -10,15 +10,15 @@ import { cn, getRoleLocalizationKeys } from '~/utils';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
interface AccessRolesPickerProps { interface AccessRolesPickerProps {
resourceType?: string; resourceType?: ResourceType;
selectedRoleId?: ACCESS_ROLE_IDS; selectedRoleId?: AccessRoleIds;
onRoleChange: (roleId: ACCESS_ROLE_IDS) => void; onRoleChange: (roleId: AccessRoleIds) => void;
className?: string; className?: string;
} }
export default function AccessRolesPicker({ export default function AccessRolesPicker({
resourceType = 'agent', resourceType = ResourceType.AGENT,
selectedRoleId = ACCESS_ROLE_IDS.AGENT_VIEWER, selectedRoleId = AccessRoleIds.AGENT_VIEWER,
onRoleChange, onRoleChange,
className = '', className = '',
}: AccessRolesPickerProps) { }: AccessRolesPickerProps) {
@ -27,7 +27,7 @@ export default function AccessRolesPicker({
const { data: accessRoles, isLoading: rolesLoading } = useGetAccessRolesQuery(resourceType); const { data: accessRoles, isLoading: rolesLoading } = useGetAccessRolesQuery(resourceType);
/** Helper function to get localized role name and description */ /** Helper function to get localized role name and description */
const getLocalizedRoleInfo = (roleId: ACCESS_ROLE_IDS) => { const getLocalizedRoleInfo = (roleId: AccessRoleIds) => {
const keys = getRoleLocalizationKeys(roleId); const keys = getRoleLocalizationKeys(roleId);
return { return {
name: localize(keys.name), name: localize(keys.name),

View file

@ -1,4 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react'; import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react';
import { import {
Button, Button,
@ -10,13 +11,17 @@ import {
useToastContext, useToastContext,
} from '@librechat/client'; } from '@librechat/client';
import type { TPrincipal } from 'librechat-data-provider'; import type { TPrincipal } from 'librechat-data-provider';
import { useLocalize, useCopyToClipboard } from '~/hooks'; import {
import { usePeoplePickerPermissions, useResourcePermissionState } from '~/hooks/Sharing'; usePeoplePickerPermissions,
useResourcePermissionState,
useCopyToClipboard,
useLocalize,
} from '~/hooks';
import GenericManagePermissionsDialog from './GenericManagePermissionsDialog'; import GenericManagePermissionsDialog from './GenericManagePermissionsDialog';
import PeoplePicker from '../SidePanel/Agents/Sharing/PeoplePicker/PeoplePicker';
import AccessRolesPicker from '../SidePanel/Agents/Sharing/AccessRolesPicker';
import PublicSharingToggle from './PublicSharingToggle'; import PublicSharingToggle from './PublicSharingToggle';
import AccessRolesPicker from './AccessRolesPicker';
import { cn, removeFocusOutlines } from '~/utils'; import { cn, removeFocusOutlines } from '~/utils';
import { PeoplePicker } from './PeoplePicker';
export default function GenericGrantAccessDialog({ export default function GenericGrantAccessDialog({
resourceName, resourceName,
@ -30,8 +35,8 @@ export default function GenericGrantAccessDialog({
resourceDbId?: string | null; resourceDbId?: string | null;
resourceId?: string | null; resourceId?: string | null;
resourceName?: string; resourceName?: string;
resourceType: string; resourceType: ResourceType;
onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void; onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: AccessRoleIds) => void;
disabled?: boolean; disabled?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
}) { }) {
@ -55,8 +60,8 @@ export default function GenericGrantAccessDialog({
} = useResourcePermissionState(resourceType, resourceDbId, isModalOpen); } = useResourcePermissionState(resourceType, resourceDbId, isModalOpen);
const [newShares, setNewShares] = useState<TPrincipal[]>([]); const [newShares, setNewShares] = useState<TPrincipal[]>([]);
const [defaultPermissionId, setDefaultPermissionId] = useState<string>( const [defaultPermissionId, setDefaultPermissionId] = useState<AccessRoleIds | undefined>(
config?.defaultViewerRoleId ?? '', config?.defaultViewerRoleId,
); );
const resourceUrl = config?.getResourceUrl ? config?.getResourceUrl(resourceId || '') : ''; const resourceUrl = config?.getResourceUrl ? config?.getResourceUrl(resourceId || '') : '';

View file

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { TPrincipal } from 'librechat-data-provider';
import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react';
import { useGetAccessRolesQuery } from 'librechat-data-provider/react-query'; import { useGetAccessRolesQuery } from 'librechat-data-provider/react-query';
import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react';
import { import {
Button, Button,
OGDialog, OGDialog,
@ -11,9 +10,10 @@ import {
OGDialogTrigger, OGDialogTrigger,
useToastContext, useToastContext,
} from '@librechat/client'; } from '@librechat/client';
import SelectedPrincipalsList from '../SidePanel/Agents/Sharing/PeoplePicker/SelectedPrincipalsList'; import type { TPrincipal, ResourceType, AccessRoleIds } from 'librechat-data-provider';
import { useResourcePermissionState } from '~/hooks/Sharing'; import { useResourcePermissionState } from '~/hooks/Sharing';
import PublicSharingToggle from './PublicSharingToggle'; import PublicSharingToggle from './PublicSharingToggle';
import { SelectedPrincipalsList } from './PeoplePicker';
import { cn, removeFocusOutlines } from '~/utils'; import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
@ -26,8 +26,12 @@ export default function GenericManagePermissionsDialog({
}: { }: {
resourceDbId: string; resourceDbId: string;
resourceName?: string; resourceName?: string;
resourceType: string; resourceType: ResourceType;
onUpdatePermissions?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void; onUpdatePermissions?: (
shares: TPrincipal[],
isPublic: boolean,
publicRole: AccessRoleIds,
) => void;
children?: React.ReactNode; children?: React.ReactNode;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();

View file

@ -1,6 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { ACCESS_ROLE_IDS, PermissionTypes, Permissions } from 'librechat-data-provider';
import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react'; import { Share2Icon, Users, Loader, Shield, Link, CopyCheck } from 'lucide-react';
import { Permissions, ResourceType, PermissionTypes, AccessRoleIds } from 'librechat-data-provider';
import { import {
useGetResourcePermissionsQuery, useGetResourcePermissionsQuery,
useUpdateResourcePermissionsMutation, useUpdateResourcePermissionsMutation,
@ -18,22 +18,22 @@ import type { TPrincipal } from 'librechat-data-provider';
import { useLocalize, useCopyToClipboard, useHasAccess } from '~/hooks'; import { useLocalize, useCopyToClipboard, useHasAccess } from '~/hooks';
import ManagePermissionsDialog from './ManagePermissionsDialog'; import ManagePermissionsDialog from './ManagePermissionsDialog';
import PublicSharingToggle from './PublicSharingToggle'; import PublicSharingToggle from './PublicSharingToggle';
import PeoplePicker from './PeoplePicker/PeoplePicker';
import AccessRolesPicker from './AccessRolesPicker'; import AccessRolesPicker from './AccessRolesPicker';
import { cn, removeFocusOutlines } from '~/utils'; import { cn, removeFocusOutlines } from '~/utils';
import { PeoplePicker } from './PeoplePicker';
export default function GrantAccessDialog({ export default function GrantAccessDialog({
agentName, agentName,
onGrantAccess, onGrantAccess,
resourceType = 'agent', resourceType = ResourceType.AGENT,
agentDbId, agentDbId,
agentId, agentId,
}: { }: {
agentDbId?: string | null; agentDbId?: string | null;
agentId?: string | null; agentId?: string | null;
agentName?: string; agentName?: string;
onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void; onGrantAccess?: (shares: TPrincipal[], isPublic: boolean, publicRole: AccessRoleIds) => void;
resourceType?: string; resourceType?: ResourceType;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
@ -73,7 +73,7 @@ export default function GrantAccessDialog({
const [newShares, setNewShares] = useState<TPrincipal[]>([]); const [newShares, setNewShares] = useState<TPrincipal[]>([]);
const [defaultPermissionId, setDefaultPermissionId] = useState<string>( const [defaultPermissionId, setDefaultPermissionId] = useState<string>(
ACCESS_ROLE_IDS.AGENT_VIEWER, AccessRoleIds.AGENT_VIEWER,
); );
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isCopying, setIsCopying] = useState(false); const [isCopying, setIsCopying] = useState(false);
@ -94,10 +94,10 @@ export default function GrantAccessDialog({
})) || []; })) || [];
const currentIsPublic = permissionsData?.public ?? false; const currentIsPublic = permissionsData?.public ?? false;
const currentPublicRole = permissionsData?.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER; const currentPublicRole = permissionsData?.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
const [isPublic, setIsPublic] = useState(false); const [isPublic, setIsPublic] = useState(false);
const [publicRole, setPublicRole] = useState<string>(ACCESS_ROLE_IDS.AGENT_VIEWER); const [publicRole, setPublicRole] = useState<AccessRoleIds>(AccessRoleIds.AGENT_VIEWER);
useEffect(() => { useEffect(() => {
if (permissionsData && isModalOpen) { if (permissionsData && isModalOpen) {
@ -140,9 +140,9 @@ export default function GrantAccessDialog({
}); });
setNewShares([]); setNewShares([]);
setDefaultPermissionId(ACCESS_ROLE_IDS.AGENT_VIEWER); setDefaultPermissionId(AccessRoleIds.AGENT_VIEWER);
setIsPublic(false); setIsPublic(false);
setPublicRole(ACCESS_ROLE_IDS.AGENT_VIEWER); setPublicRole(AccessRoleIds.AGENT_VIEWER);
setIsModalOpen(false); setIsModalOpen(false);
} catch (error) { } catch (error) {
console.error('Error granting access:', error); console.error('Error granting access:', error);
@ -155,9 +155,9 @@ export default function GrantAccessDialog({
const handleCancel = () => { const handleCancel = () => {
setNewShares([]); setNewShares([]);
setDefaultPermissionId(ACCESS_ROLE_IDS.AGENT_VIEWER); setDefaultPermissionId(AccessRoleIds.AGENT_VIEWER);
setIsPublic(false); setIsPublic(false);
setPublicRole(ACCESS_ROLE_IDS.AGENT_VIEWER); setPublicRole(AccessRoleIds.AGENT_VIEWER);
setIsModalOpen(false); setIsModalOpen(false);
}; };

View file

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { ACCESS_ROLE_IDS, TPrincipal } from 'librechat-data-provider'; import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react'; import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react';
import { import {
useGetAccessRolesQuery, useGetAccessRolesQuery,
useGetResourcePermissionsQuery, useGetResourcePermissionsQuery,
useUpdateResourcePermissionsMutation, useUpdateResourcePermissionsMutation,
} from 'librechat-data-provider/react-query'; } from 'librechat-data-provider/react-query';
import type { TPrincipal } from 'librechat-data-provider';
import { import {
Button, Button,
OGDialog, OGDialog,
@ -15,21 +16,25 @@ import {
OGDialogTrigger, OGDialogTrigger,
useToastContext, useToastContext,
} from '@librechat/client'; } from '@librechat/client';
import SelectedPrincipalsList from './PeoplePicker/SelectedPrincipalsList'; import { SelectedPrincipalsList } from './PeoplePicker';
import PublicSharingToggle from './PublicSharingToggle'; import PublicSharingToggle from './PublicSharingToggle';
import { cn, removeFocusOutlines } from '~/utils'; import { cn, removeFocusOutlines } from '~/utils';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
export default function ManagePermissionsDialog({ export default function ManagePermissionsDialog({
agentDbId,
agentName, agentName,
resourceType = 'agent', resourceType = ResourceType.AGENT,
agentDbId,
onUpdatePermissions, onUpdatePermissions,
}: { }: {
agentDbId: string; agentDbId: string;
agentName?: string; agentName?: string;
resourceType?: string; resourceType?: ResourceType;
onUpdatePermissions?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void; onUpdatePermissions?: (
shares: TPrincipal[],
isPublic: boolean,
publicRole: AccessRoleIds,
) => void;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
@ -50,20 +55,22 @@ export default function ManagePermissionsDialog({
const [managedShares, setManagedShares] = useState<TPrincipal[]>([]); const [managedShares, setManagedShares] = useState<TPrincipal[]>([]);
const [managedIsPublic, setManagedIsPublic] = useState(false); const [managedIsPublic, setManagedIsPublic] = useState(false);
const [managedPublicRole, setManagedPublicRole] = useState<string>(ACCESS_ROLE_IDS.AGENT_VIEWER); const [managedPublicRole, setManagedPublicRole] = useState<AccessRoleIds>(
AccessRoleIds.AGENT_VIEWER,
);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const currentShares: TPrincipal[] = permissionsData?.principals || []; const currentShares: TPrincipal[] = permissionsData?.principals || [];
const isPublic = permissionsData?.public || false; const isPublic = permissionsData?.public || false;
const publicRole = permissionsData?.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER; const publicRole = permissionsData?.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
useEffect(() => { useEffect(() => {
if (permissionsData) { if (permissionsData) {
const shares = permissionsData.principals || []; const shares = permissionsData.principals || [];
const isPublicValue = permissionsData.public || false; const isPublicValue = permissionsData.public || false;
const publicRoleValue = permissionsData.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER; const publicRoleValue = permissionsData.publicAccessRoleId || AccessRoleIds.AGENT_VIEWER;
setManagedShares(shares); setManagedShares(shares);
setManagedIsPublic(isPublicValue); setManagedIsPublic(isPublicValue);
@ -85,7 +92,7 @@ export default function ManagePermissionsDialog({
setHasChanges(true); setHasChanges(true);
}; };
const handleRoleChange = (idOnTheSource: string, newRole: string) => { const handleRoleChange = (idOnTheSource: string, newRole: AccessRoleIds) => {
setManagedShares( setManagedShares(
managedShares.map((s) => managedShares.map((s) =>
s.idOnTheSource === idOnTheSource ? { ...s, accessRoleId: newRole } : s, s.idOnTheSource === idOnTheSource ? { ...s, accessRoleId: newRole } : s,
@ -160,10 +167,10 @@ export default function ManagePermissionsDialog({
setManagedIsPublic(isPublic); setManagedIsPublic(isPublic);
setHasChanges(true); setHasChanges(true);
if (!isPublic) { if (!isPublic) {
setManagedPublicRole(ACCESS_ROLE_IDS.AGENT_VIEWER); setManagedPublicRole(AccessRoleIds.AGENT_VIEWER);
} }
}; };
const handlePublicRoleChange = (role: string) => { const handlePublicRoleChange = (role: AccessRoleIds) => {
setManagedPublicRole(role); setManagedPublicRole(role);
setHasChanges(true); setHasChanges(true);
}; };
@ -172,8 +179,8 @@ export default function ManagePermissionsDialog({
/** Check if there's at least one owner (user, group, or public with owner role) */ /** Check if there's at least one owner (user, group, or public with owner role) */
const hasAtLeastOneOwner = const hasAtLeastOneOwner =
managedShares.some((share) => share.accessRoleId === ACCESS_ROLE_IDS.AGENT_OWNER) || managedShares.some((share) => share.accessRoleId === AccessRoleIds.AGENT_OWNER) ||
(managedIsPublic && managedPublicRole === ACCESS_ROLE_IDS.AGENT_OWNER); (managedIsPublic && managedPublicRole === AccessRoleIds.AGENT_OWNER);
let peopleLabel = localize('com_ui_people'); let peopleLabel = localize('com_ui_people');
if (managedShares.length === 1) { if (managedShares.length === 1) {

View file

@ -3,7 +3,7 @@ import type { TPrincipal, PrincipalSearchParams } from 'librechat-data-provider'
import { useSearchPrincipalsQuery } from 'librechat-data-provider/react-query'; import { useSearchPrincipalsQuery } from 'librechat-data-provider/react-query';
import PeoplePickerSearchItem from './PeoplePickerSearchItem'; import PeoplePickerSearchItem from './PeoplePickerSearchItem';
import SelectedPrincipalsList from './SelectedPrincipalsList'; import SelectedPrincipalsList from './SelectedPrincipalsList';
import { SearchPicker } from '~/components/ui/SearchPicker'; import { SearchPicker } from './SearchPicker';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
interface PeoplePickerProps { interface PeoplePickerProps {

View file

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import debounce from 'lodash/debounce';
import { Search, X } from 'lucide-react'; import { Search, X } from 'lucide-react';
import * as Ariakit from '@ariakit/react'; import * as Ariakit from '@ariakit/react';
import { Spinner, Skeleton } from '@librechat/client'; import { Spinner, Skeleton } from '@librechat/client';
@ -36,10 +37,31 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
const localize = useLocalize(); const localize = useLocalize();
const [_open, setOpen] = React.useState(false); const [_open, setOpen] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
const [localQuery, setLocalQuery] = React.useState(query);
const combobox = Ariakit.useComboboxStore({ const combobox = Ariakit.useComboboxStore({
resetValueOnHide, resetValueOnHide,
}); });
React.useEffect(() => {
setLocalQuery(query);
}, [query]);
const debouncedOnQueryChange = React.useMemo(
() =>
debounce((value: string) => {
onQueryChange(value);
}, 500),
[onQueryChange],
);
React.useEffect(() => {
return () => {
debouncedOnQueryChange.cancel();
};
}, [debouncedOnQueryChange]);
const onPickHandler = (option: TOption) => { const onPickHandler = (option: TOption) => {
setLocalQuery('');
onQueryChange(''); onQueryChange('');
onPick(option); onPick(option);
setOpen(false); setOpen(false);
@ -47,9 +69,11 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
inputRef.current.focus(); inputRef.current.focus();
} }
}; };
const showClearIcon = query.trim().length > 0; const showClearIcon = localQuery.trim().length > 0;
const clearText = () => { const clearText = () => {
setLocalQuery('');
onQueryChange(''); onQueryChange('');
debouncedOnQueryChange.cancel();
if (inputRef.current) { if (inputRef.current) {
inputRef.current.focus(); inputRef.current.focus();
} }
@ -77,7 +101,9 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
if (e.key === 'Escape' && combobox.getState().open) { if (e.key === 'Escape' && combobox.getState().open) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
setLocalQuery('');
onQueryChange(''); onQueryChange('');
debouncedOnQueryChange.cancel();
setOpen(false); setOpen(false);
} }
}} }}
@ -85,9 +111,11 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
setValueOnClick={false} setValueOnClick={false}
setValueOnChange={false} setValueOnChange={false}
onChange={(e) => { onChange={(e) => {
onQueryChange(e.target.value); const value = e.target.value;
setLocalQuery(value);
debouncedOnQueryChange(value);
}} }}
value={query} value={localQuery}
// autoSelect // autoSelect
placeholder={placeholder || localize('com_ui_select_options')} placeholder={placeholder || localize('com_ui_select_options')}
className="m-0 mr-0 w-full rounded-md border-none bg-transparent p-0 py-2 pl-9 pr-3 text-sm leading-tight text-text-primary placeholder-text-secondary placeholder-opacity-100 focus:outline-none focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary" className="m-0 mr-0 w-full rounded-md border-none bg-transparent p-0 py-2 pl-9 pr-3 text-sm leading-tight text-text-primary placeholder-text-secondary placeholder-opacity-100 focus:outline-none focus-visible:outline-none group-focus-within:placeholder-text-primary group-hover:placeholder-text-primary"
@ -115,7 +143,7 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
open={ open={
isLoading || isLoading ||
options.length > 0 || options.length > 0 ||
(query.trim().length >= minQueryLengthForNoResults && !isLoading) (localQuery.trim().length >= minQueryLengthForNoResults && !isLoading)
} }
store={combobox} store={combobox}
unmountOnHide unmountOnHide
@ -162,7 +190,7 @@ export function SearchPicker<TOption extends { key: string; value: string }>({
)); ));
} }
if (query.trim().length >= minQueryLengthForNoResults) { if (localQuery.trim().length >= minQueryLengthForNoResults) {
return ( return (
<div <div
className={cn( className={cn(

View file

@ -2,7 +2,7 @@ import React, { useState, useId } from 'react';
import * as Menu from '@ariakit/react/menu'; import * as Menu from '@ariakit/react/menu';
import { Button, DropdownPopup } from '@librechat/client'; import { Button, DropdownPopup } from '@librechat/client';
import { Users, X, ExternalLink, ChevronDown } from 'lucide-react'; import { Users, X, ExternalLink, ChevronDown } from 'lucide-react';
import type { TPrincipal, TAccessRole, ACCESS_ROLE_IDS } from 'librechat-data-provider'; import type { TPrincipal, TAccessRole, AccessRoleIds } from 'librechat-data-provider';
import { getRoleLocalizationKeys } from '~/utils'; import { getRoleLocalizationKeys } from '~/utils';
import PrincipalAvatar from '../PrincipalAvatar'; import PrincipalAvatar from '../PrincipalAvatar';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
@ -10,7 +10,7 @@ import { useLocalize } from '~/hooks';
interface SelectedPrincipalsListProps { interface SelectedPrincipalsListProps {
principles: TPrincipal[]; principles: TPrincipal[];
onRemoveHandler: (idOnTheSource: string) => void; onRemoveHandler: (idOnTheSource: string) => void;
onRoleChange?: (idOnTheSource: string, newRoleId: string) => void; onRoleChange?: (idOnTheSource: string, newRoleId: AccessRoleIds) => void;
availableRoles?: Omit<TAccessRole, 'resourceType'>[]; availableRoles?: Omit<TAccessRole, 'resourceType'>[];
className?: string; className?: string;
} }
@ -98,8 +98,8 @@ export default function SelectedPrincipalsList({
} }
interface RoleSelectorProps { interface RoleSelectorProps {
currentRole: ACCESS_ROLE_IDS; currentRole: AccessRoleIds;
onRoleChange: (newRole: ACCESS_ROLE_IDS) => void; onRoleChange: (newRole: AccessRoleIds) => void;
availableRoles: Omit<TAccessRole, 'resourceType'>[]; availableRoles: Omit<TAccessRole, 'resourceType'>[];
} }
@ -108,7 +108,7 @@ function RoleSelector({ currentRole, onRoleChange, availableRoles }: RoleSelecto
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const localize = useLocalize(); const localize = useLocalize();
const getLocalizedRoleName = (roleId: ACCESS_ROLE_IDS) => { const getLocalizedRoleName = (roleId: AccessRoleIds) => {
const keys = getRoleLocalizationKeys(roleId); const keys = getRoleLocalizationKeys(roleId);
return localize(keys.name); return localize(keys.name);
}; };

View file

@ -0,0 +1,3 @@
export { default as PeoplePicker } from './PeoplePicker';
export { default as PeoplePickerSearchItem } from './PeoplePickerSearchItem';
export { default as SelectedPrincipalsList } from './SelectedPrincipalsList';

View file

@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import { Globe, Shield } from 'lucide-react';
import { Switch } from '@librechat/client'; import { Switch } from '@librechat/client';
import AccessRolesPicker from '../SidePanel/Agents/Sharing/AccessRolesPicker'; import { Globe, Shield } from 'lucide-react';
import type { AccessRoleIds } from 'librechat-data-provider';
import { ResourceType } from 'librechat-data-provider';
import AccessRolesPicker from './AccessRolesPicker';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
export default function PublicSharingToggle({ export default function PublicSharingToggle({
@ -9,13 +11,13 @@ export default function PublicSharingToggle({
publicRole, publicRole,
onPublicToggle, onPublicToggle,
onPublicRoleChange, onPublicRoleChange,
resourceType = 'agent', resourceType = ResourceType.AGENT,
}: { }: {
isPublic: boolean; isPublic: boolean;
publicRole: string; publicRole: AccessRoleIds;
onPublicToggle: (isPublic: boolean) => void; onPublicToggle: (isPublic: boolean) => void;
onPublicRoleChange: (role: string) => void; onPublicRoleChange: (role: AccessRoleIds) => void;
resourceType?: string; resourceType?: ResourceType;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();

View file

@ -1,3 +1,6 @@
export { default as AccessRolesPicker } from './AccessRolesPicker';
export { default as GenericGrantAccessDialog } from './GenericGrantAccessDialog'; export { default as GenericGrantAccessDialog } from './GenericGrantAccessDialog';
export { default as GenericManagePermissionsDialog } from './GenericManagePermissionsDialog'; export { default as GenericManagePermissionsDialog } from './GenericManagePermissionsDialog';
export { default as ManagePermissionsDialog } from './ManagePermissionsDialog';
export { default as PrincipalAvatar } from './PrincipalAvatar';
export { default as PublicSharingToggle } from './PublicSharingToggle'; export { default as PublicSharingToggle } from './PublicSharingToggle';

View file

@ -1,16 +1,16 @@
import * as Ariakit from '@ariakit/react';
import { useMemo, useEffect, useState } from 'react'; import { useMemo, useEffect, useState } from 'react';
import * as Ariakit from '@ariakit/react';
import { ShieldEllipsis } from 'lucide-react'; import { ShieldEllipsis } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider'; import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider';
import { import {
Button,
Switch,
OGDialog, OGDialog,
DropdownPopup,
OGDialogTitle, OGDialogTitle,
OGDialogContent, OGDialogContent,
OGDialogTrigger, OGDialogTrigger,
Button,
Switch,
DropdownPopup,
useToastContext, useToastContext,
} from '@librechat/client'; } from '@librechat/client';
import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form'; import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form';
@ -64,8 +64,8 @@ const LabelController: React.FC<LabelControllerProps> = ({
const AdminSettings = () => { const AdminSettings = () => {
const localize = useLocalize(); const localize = useLocalize();
const { user, roles } = useAuthContext();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
const { user, roles } = useAuthContext();
const { mutate, isLoading } = useUpdateAgentPermissionsMutation({ const { mutate, isLoading } = useUpdateAgentPermissionsMutation({
onSuccess: () => { onSuccess: () => {
showToast({ status: 'success', message: localize('com_ui_saved') }); showToast({ status: 'success', message: localize('com_ui_saved') });
@ -79,8 +79,9 @@ const AdminSettings = () => {
const [selectedRole, setSelectedRole] = useState<SystemRoles>(SystemRoles.USER); const [selectedRole, setSelectedRole] = useState<SystemRoles>(SystemRoles.USER);
const defaultValues = useMemo(() => { const defaultValues = useMemo(() => {
if (roles?.[selectedRole]?.permissions) { const rolePerms = roles?.[selectedRole]?.permissions;
return roles[selectedRole].permissions[PermissionTypes.AGENTS]; if (rolePerms) {
return rolePerms[PermissionTypes.AGENTS];
} }
return roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS]; return roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS];
}, [roles, selectedRole]); }, [roles, selectedRole]);
@ -98,8 +99,9 @@ const AdminSettings = () => {
}); });
useEffect(() => { useEffect(() => {
if (roles?.[selectedRole]?.permissions?.[PermissionTypes.AGENTS]) { const value = roles?.[selectedRole]?.permissions?.[PermissionTypes.AGENTS];
reset(roles[selectedRole].permissions[PermissionTypes.AGENTS]); if (value) {
reset(value);
} else { } else {
reset(roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS]); reset(roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS]);
} }
@ -211,7 +213,8 @@ const AdminSettings = () => {
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">
<button <button
type="submit" type="button"
onClick={handleSubmit(onSubmit)}
disabled={isSubmitting || isLoading} disabled={isSubmitting || isLoading}
className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600" className="btn rounded bg-green-500 font-bold text-white transition-all hover:bg-green-600"
> >

View file

@ -3,8 +3,9 @@ import { useWatch, useFormContext } from 'react-hook-form';
import { import {
SystemRoles, SystemRoles,
Permissions, Permissions,
ResourceType,
PermissionTypes, PermissionTypes,
PERMISSION_BITS, PermissionBits,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { AgentForm, AgentPanelProps } from '~/common'; import type { AgentForm, AgentPanelProps } from '~/common';
import { useLocalize, useAuthContext, useHasAccess, useResourcePermissions } from '~/hooks'; import { useLocalize, useAuthContext, useHasAccess, useResourcePermissions } from '~/hooks';
@ -46,8 +47,8 @@ export default function AgentFooter({
agent?._id || '', agent?._id || '',
); );
const canShareThisAgent = hasPermission(PERMISSION_BITS.SHARE); const canShareThisAgent = hasPermission(PermissionBits.SHARE);
const canDeleteThisAgent = hasPermission(PERMISSION_BITS.DELETE); const canDeleteThisAgent = hasPermission(PermissionBits.DELETE);
const renderSaveButton = () => { const renderSaveButton = () => {
if (createMutation.isLoading || updateMutation.isLoading) { if (createMutation.isLoading || updateMutation.isLoading) {
return <Spinner className="icon-md" aria-hidden="true" />; return <Spinner className="icon-md" aria-hidden="true" />;
@ -84,7 +85,7 @@ export default function AgentFooter({
resourceDbId={agent?._id} resourceDbId={agent?._id}
resourceId={agent_id} resourceId={agent_id}
resourceName={agent?.name ?? ''} resourceName={agent?.name ?? ''}
resourceType="agent" resourceType={ResourceType.AGENT}
/> />
)} )}
{agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />} {agent && agent.author === user?.id && <DuplicateAgent agent_id={agent_id} />}

View file

@ -8,7 +8,7 @@ import {
Constants, Constants,
SystemRoles, SystemRoles,
EModelEndpoint, EModelEndpoint,
PERMISSION_BITS, PermissionBits,
isAssistantsEndpoint, isAssistantsEndpoint,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { AgentForm, StringOption } from '~/common'; import type { AgentForm, StringOption } from '~/common';
@ -57,7 +57,7 @@ export default function AgentPanel() {
basicAgentQuery.data?._id || '', basicAgentQuery.data?._id || '',
); );
const canEdit = hasPermission(PERMISSION_BITS.EDIT); const canEdit = hasPermission(PermissionBits.EDIT);
const expandedAgentQuery = useGetExpandedAgentByIdQuery(current_agent_id ?? '', { const expandedAgentQuery = useGetExpandedAgentByIdQuery(current_agent_id ?? '', {
enabled: enabled:

View file

@ -7,7 +7,6 @@ import { Controller, useFormContext } from 'react-hook-form';
import type { TSpecialVarLabel } from 'librechat-data-provider'; import type { TSpecialVarLabel } from 'librechat-data-provider';
import type { AgentForm } from '~/common'; import type { AgentForm } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines } from '~/utils'; import { cn, defaultTextProps, removeFocusOutlines } from '~/utils';
// import { ControlCombobox } from '@librechat/client';
import { useLocalize } from '~/hooks'; import { useLocalize } from '~/hooks';
const inputClass = cn( const inputClass = cn(
@ -49,26 +48,6 @@ export default function Instructions() {
{localize('com_ui_instructions')} {localize('com_ui_instructions')}
</label> </label>
<div className="ml-auto" title="Add variables to instructions"> <div className="ml-auto" title="Add variables to instructions">
{/* ControlCombobox implementation
<ControlCombobox
selectedValue=""
displayValue="Add variables"
items={variableOptions.map((option) => ({
label: option.label,
value: option.value,
}))}
setValue={handleAddVariable}
ariaLabel="Add variable to instructions"
searchPlaceholder="Search variables"
selectPlaceholder="Add"
isCollapsed={false}
SelectIcon={<PlusCircle className="h-3 w-3 text-text-secondary" />}
containerClassName="w-fit"
className="h-7 gap-1 rounded-md border border-border-medium bg-surface-secondary px-2 py-0 text-sm text-text-primary transition-colors duration-200 hover:bg-surface-tertiary"
iconSide="left"
showCarat={false}
/>
*/}
<DropdownPopup <DropdownPopup
portal={true} portal={true}
mountByState={true} mountByState={true}

View file

@ -1,59 +0,0 @@
import React from 'react';
import { Globe } from 'lucide-react';
import { Switch } from '@librechat/client';
import AccessRolesPicker from './AccessRolesPicker';
import { useLocalize } from '~/hooks';
interface PublicSharingToggleProps {
isPublic: boolean;
publicRole: string;
onPublicToggle: (isPublic: boolean) => void;
onPublicRoleChange: (role: string) => void;
className?: string;
resourceType?: string;
}
export default function PublicSharingToggle({
isPublic,
publicRole,
onPublicToggle,
onPublicRoleChange,
className = '',
resourceType = 'agent',
}: PublicSharingToggleProps) {
const localize = useLocalize();
return (
<div className={`space-y-3 border-t pt-4 ${className}`}>
<div className="flex items-center justify-between">
<div>
<h3 className="flex items-center gap-2 text-sm font-medium">
<Globe className="h-4 w-4" />
{localize('com_ui_share_with_everyone')}
</h3>
<p className="mt-1 text-xs text-muted-foreground">
{localize('com_ui_make_agent_available_all_users')}
</p>
</div>
<Switch
checked={isPublic}
onCheckedChange={onPublicToggle}
aria-label={localize('com_ui_share_with_everyone')}
/>
</div>
{isPublic && (
<div>
<label className="mb-2 block text-sm font-medium">
{localize('com_ui_public_access_level')}
</label>
<AccessRolesPicker
resourceType={resourceType}
selectedRoleId={publicRole}
onRoleChange={onPublicRoleChange}
/>
</div>
)}
</div>
);
}

View file

@ -3,7 +3,7 @@ import { SystemRoles } from 'librechat-data-provider';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import type { UseMutationResult } from '@tanstack/react-query'; import type { UseMutationResult } from '@tanstack/react-query';
import '@testing-library/jest-dom/extend-expect'; import '@testing-library/jest-dom/extend-expect';
import type { Agent, AgentCreateParams, TUser } from 'librechat-data-provider'; import type { Agent, AgentCreateParams, TUser, ResourceType } from 'librechat-data-provider';
import AgentFooter from '../AgentFooter'; import AgentFooter from '../AgentFooter';
import { Panel } from '~/common'; import { Panel } from '~/common';
@ -171,7 +171,7 @@ jest.mock('~/components/Sharing', () => ({
resourceDbId: string; resourceDbId: string;
resourceId: string; resourceId: string;
resourceName: string; resourceName: string;
resourceType: string; resourceType: ResourceType;
}) => ( }) => (
<div <div
data-testid="grant-access-dialog" data-testid="grant-access-dialog"

View file

@ -1,3 +1,2 @@
export * from './ui'; export * from './ui';
export * from './Plugins'; export * from './Plugins';
export * from './svg';

View file

@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { dataService, MutationKeys, PERMISSION_BITS, QueryKeys } from 'librechat-data-provider'; import { dataService, MutationKeys, PermissionBits, QueryKeys } from 'librechat-data-provider';
import type * as t from 'librechat-data-provider'; import type * as t from 'librechat-data-provider';
import type { QueryClient, UseMutationResult } from '@tanstack/react-query'; import type { QueryClient, UseMutationResult } from '@tanstack/react-query';
@ -7,8 +7,8 @@ import type { QueryClient, UseMutationResult } from '@tanstack/react-query';
* AGENTS * AGENTS
*/ */
export const allAgentViewAndEditQueryKeys: t.AgentListParams[] = [ export const allAgentViewAndEditQueryKeys: t.AgentListParams[] = [
{ requiredPermission: PERMISSION_BITS.VIEW }, { requiredPermission: PermissionBits.VIEW },
{ requiredPermission: PERMISSION_BITS.EDIT }, { requiredPermission: PermissionBits.EDIT },
]; ];
/** /**
* Create a new agent * Create a new agent

View file

@ -3,7 +3,7 @@ import {
dataService, dataService,
Permissions, Permissions,
EModelEndpoint, EModelEndpoint,
PERMISSION_BITS, PermissionBits,
PermissionTypes, PermissionTypes,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
@ -26,7 +26,7 @@ export const useAgentListingDefaultPermissionLevel = () => {
// When marketplace is active: EDIT permissions (builder mode) // When marketplace is active: EDIT permissions (builder mode)
// When marketplace is not active: VIEW permissions (browse mode) // When marketplace is not active: VIEW permissions (browse mode)
return hasMarketplaceAccess ? PERMISSION_BITS.EDIT : PERMISSION_BITS.VIEW; return hasMarketplaceAccess ? PermissionBits.EDIT : PermissionBits.VIEW;
}; };
/** /**
@ -34,7 +34,7 @@ export const useAgentListingDefaultPermissionLevel = () => {
*/ */
export const defaultAgentParams: t.AgentListParams = { export const defaultAgentParams: t.AgentListParams = {
limit: 10, limit: 10,
requiredPermission: PERMISSION_BITS.EDIT, requiredPermission: PermissionBits.EDIT,
}; };
/** /**
* Hook for getting all available tools for A * Hook for getting all available tools for A

View file

@ -8,7 +8,7 @@ import {
isAgentsEndpoint, isAgentsEndpoint,
getConfigDefaults, getConfigDefaults,
isAssistantsEndpoint, isAssistantsEndpoint,
PERMISSION_BITS, PermissionBits,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider'; import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { MentionOption } from '~/common'; import type { MentionOption } from '~/common';
@ -81,7 +81,7 @@ export default function useMentions({
[startupConfig?.interface], [startupConfig?.interface],
); );
const { data: agentsList = null } = useListAgentsQuery( const { data: agentsList = null } = useListAgentsQuery(
{ requiredPermission: PERMISSION_BITS.VIEW }, { requiredPermission: PermissionBits.VIEW },
{ {
enabled: hasAgentAccess && interfaceConfig.modelSelect === true, enabled: hasAgentAccess && interfaceConfig.modelSelect === true,
select: (res) => { select: (res) => {

View file

@ -8,7 +8,7 @@ import {
isAgentsEndpoint, isAgentsEndpoint,
tQueryParamsSchema, tQueryParamsSchema,
isAssistantsEndpoint, isAssistantsEndpoint,
PERMISSION_BITS, PermissionBits,
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { import type {
TPreset, TPreset,
@ -80,7 +80,7 @@ const processValidSettings = (queryParams: Record<string, string>) => {
}; };
const injectAgentIntoAgentsMap = (queryClient: QueryClient, agent: any) => { const injectAgentIntoAgentsMap = (queryClient: QueryClient, agent: any) => {
const editCacheKey = [QueryKeys.agents, { requiredPermission: PERMISSION_BITS.EDIT }]; const editCacheKey = [QueryKeys.agents, { requiredPermission: PermissionBits.EDIT }];
const editCache = queryClient.getQueryData<AgentListResponse>(editCacheKey); const editCache = queryClient.getQueryData<AgentListResponse>(editCacheKey);
if (editCache?.data && !editCache.data.some((cachedAgent) => cachedAgent.id === agent.id)) { if (editCache?.data && !editCache.data.some((cachedAgent) => cachedAgent.id === agent.id)) {

View file

@ -3,18 +3,18 @@ import {
useGetResourcePermissionsQuery, useGetResourcePermissionsQuery,
useUpdateResourcePermissionsMutation, useUpdateResourcePermissionsMutation,
} from 'librechat-data-provider/react-query'; } from 'librechat-data-provider/react-query';
import type { TPrincipal } from 'librechat-data-provider'; import type { TPrincipal, ResourceType, AccessRoleIds } from 'librechat-data-provider';
import { getResourceConfig } from '~/utils'; import { getResourceConfig } from '~/utils';
/** /**
* Hook to manage resource permission state including current shares, public access, and mutations * Hook to manage resource permission state including current shares, public access, and mutations
* @param resourceType - Type of resource (e.g., 'agent', 'promptGroup') * @param resourceType - Type of resource (e.g., ResourceType.AGENT, ResourceType.PROMPTGROUP)
* @param resourceDbId - Database ID of the resource * @param resourceDbId - Database ID of the resource
* @param isModalOpen - Whether the modal is open (for effect dependencies) * @param isModalOpen - Whether the modal is open (for effect dependencies)
* @returns Object with permission state and update mutation * @returns Object with permission state and update mutation
*/ */
export const useResourcePermissionState = ( export const useResourcePermissionState = (
resourceType: string, resourceType: ResourceType,
resourceDbId: string | null | undefined, resourceDbId: string | null | undefined,
isModalOpen: boolean = false, isModalOpen: boolean = false,
) => { ) => {
@ -52,13 +52,15 @@ export const useResourcePermissionState = (
// State for managing public access // State for managing public access
const [isPublic, setIsPublic] = useState(false); const [isPublic, setIsPublic] = useState(false);
const [publicRole, setPublicRole] = useState<string>(config?.defaultViewerRoleId ?? ''); const [publicRole, setPublicRole] = useState<AccessRoleIds | undefined>(
config?.defaultViewerRoleId,
);
// Sync state with permissions data when modal opens // Sync state with permissions data when modal opens
useEffect(() => { useEffect(() => {
if (permissionsData && isModalOpen) { if (permissionsData && isModalOpen) {
setIsPublic(currentIsPublic ?? false); setIsPublic(currentIsPublic ?? false);
setPublicRole(currentPublicRole ?? ''); setPublicRole(currentPublicRole);
} }
}, [permissionsData, isModalOpen, currentIsPublic, currentPublicRole]); }, [permissionsData, isModalOpen, currentIsPublic, currentPublicRole]);

View file

@ -13,6 +13,7 @@ export * from './Messages';
export * from './Plugins'; export * from './Plugins';
export * from './Prompts'; export * from './Prompts';
export * from './Roles'; export * from './Roles';
export * from './Sharing';
export * from './SSE'; export * from './SSE';
export * from './AuthContext'; export * from './AuthContext';
export * from './ScreenshotContext'; export * from './ScreenshotContext';

View file

@ -1,16 +1,17 @@
import { import {
useGetEffectivePermissionsQuery,
hasPermissions, hasPermissions,
useGetEffectivePermissionsQuery,
} from 'librechat-data-provider/react-query'; } from 'librechat-data-provider/react-query';
import type { ResourceType } from 'librechat-data-provider';
/** /**
* fetches resource permissions once and returns a function to check any permission * fetches resource permissions once and returns a function to check any permission
* More efficient when checking multiple permissions for the same resource * More efficient when checking multiple permissions for the same resource
* @param resourceType - Type of resource (e.g., 'agent') * @param resourceType - Type of resource (e.g., ResourceType.AGENT)
* @param resourceId - ID of the resource * @param resourceId - ID of the resource
* @returns Object with hasPermission function and loading state * @returns Object with hasPermission function and loading state
*/ */
export const useResourcePermissions = (resourceType: string, resourceId: string) => { export const useResourcePermissions = (resourceType: ResourceType, resourceId: string) => {
const { data, isLoading } = useGetEffectivePermissionsQuery(resourceType, resourceId); const { data, isLoading } = useGetEffectivePermissionsQuery(resourceType, resourceId);
const hasPermission = (requiredPermission: number): boolean => { const hasPermission = (requiredPermission: number): boolean => {

View file

@ -8,6 +8,7 @@ import {
TwoFactorScreen, TwoFactorScreen,
RequestPasswordReset, RequestPasswordReset,
} from '~/components/Auth'; } from '~/components/Auth';
import AgentMarketplace from '~/components/Agents/Marketplace';
import { OAuthSuccess, OAuthError } from '~/components/OAuth'; import { OAuthSuccess, OAuthError } from '~/components/OAuth';
import { AuthContextProvider } from '~/hooks/AuthContext'; import { AuthContextProvider } from '~/hooks/AuthContext';
import RouteErrorBoundary from './RouteErrorBoundary'; import RouteErrorBoundary from './RouteErrorBoundary';
@ -18,7 +19,6 @@ import ShareRoute from './ShareRoute';
import ChatRoute from './ChatRoute'; import ChatRoute from './ChatRoute';
import Search from './Search'; import Search from './Search';
import Root from './Root'; import Root from './Root';
import AgentMarketplace from '~/components/SidePanel/Agents/AgentMarketplace';
const AuthLayout = () => ( const AuthLayout = () => (
<AuthContextProvider> <AuthContextProvider>

View file

@ -1,10 +1,10 @@
import { ACCESS_ROLE_IDS } from 'librechat-data-provider'; import { AccessRoleIds, ResourceType } from 'librechat-data-provider';
export interface ResourceConfig { export interface ResourceConfig {
resourceType: string; resourceType: ResourceType;
defaultViewerRoleId: string; defaultViewerRoleId: AccessRoleIds;
defaultEditorRoleId: string; defaultEditorRoleId: AccessRoleIds;
defaultOwnerRoleId: string; defaultOwnerRoleId: AccessRoleIds;
getResourceUrl?: (resourceId: string) => string; getResourceUrl?: (resourceId: string) => string;
getResourceName: (resourceName?: string) => string; getResourceName: (resourceName?: string) => string;
getShareMessage: (resourceName?: string) => string; getShareMessage: (resourceName?: string) => string;
@ -12,12 +12,12 @@ export interface ResourceConfig {
getCopyUrlMessage: () => string; getCopyUrlMessage: () => string;
} }
export const RESOURCE_CONFIGS: Record<string, ResourceConfig> = { export const RESOURCE_CONFIGS: Record<ResourceType, ResourceConfig> = {
agent: { [ResourceType.AGENT]: {
resourceType: 'agent', resourceType: ResourceType.AGENT,
defaultViewerRoleId: ACCESS_ROLE_IDS.AGENT_VIEWER, defaultViewerRoleId: AccessRoleIds.AGENT_VIEWER,
defaultEditorRoleId: ACCESS_ROLE_IDS.AGENT_EDITOR, defaultEditorRoleId: AccessRoleIds.AGENT_EDITOR,
defaultOwnerRoleId: ACCESS_ROLE_IDS.AGENT_OWNER, defaultOwnerRoleId: AccessRoleIds.AGENT_OWNER,
getResourceUrl: (agentId: string) => `${window.location.origin}/c/new?agent_id=${agentId}`, getResourceUrl: (agentId: string) => `${window.location.origin}/c/new?agent_id=${agentId}`,
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'), getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'), getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'agent'),
@ -25,11 +25,11 @@ export const RESOURCE_CONFIGS: Record<string, ResourceConfig> = {
`Manage permissions for ${name && name !== '' ? `"${name}"` : 'agent'}`, `Manage permissions for ${name && name !== '' ? `"${name}"` : 'agent'}`,
getCopyUrlMessage: () => 'Agent URL copied', getCopyUrlMessage: () => 'Agent URL copied',
}, },
promptGroup: { [ResourceType.PROMPTGROUP]: {
resourceType: 'promptGroup', resourceType: ResourceType.PROMPTGROUP,
defaultViewerRoleId: ACCESS_ROLE_IDS.PROMPTGROUP_VIEWER, defaultViewerRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
defaultEditorRoleId: ACCESS_ROLE_IDS.PROMPTGROUP_EDITOR, defaultEditorRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
defaultOwnerRoleId: ACCESS_ROLE_IDS.PROMPTGROUP_OWNER, defaultOwnerRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'), getResourceName: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'), getShareMessage: (name?: string) => (name && name !== '' ? `"${name}"` : 'prompt'),
getManageMessage: (name?: string) => getManageMessage: (name?: string) =>
@ -38,6 +38,6 @@ export const RESOURCE_CONFIGS: Record<string, ResourceConfig> = {
}, },
}; };
export const getResourceConfig = (resourceType: string): ResourceConfig | undefined => { export const getResourceConfig = (resourceType: ResourceType): ResourceConfig | undefined => {
return RESOURCE_CONFIGS[resourceType]; return RESOURCE_CONFIGS[resourceType];
}; };

View file

@ -1,4 +1,4 @@
import type { ACCESS_ROLE_IDS } from 'librechat-data-provider'; import type { AccessRoleIds } from 'librechat-data-provider';
import type { TranslationKeys } from '~/hooks/useLocalize'; import type { TranslationKeys } from '~/hooks/useLocalize';
/** /**
@ -43,7 +43,7 @@ export const ROLE_LOCALIZATIONS = {
* @returns Object with name and description localization keys, or unknown keys if not found * @returns Object with name and description localization keys, or unknown keys if not found
*/ */
export const getRoleLocalizationKeys = ( export const getRoleLocalizationKeys = (
roleId: ACCESS_ROLE_IDS, roleId: AccessRoleIds,
): { ): {
name: TranslationKeys; name: TranslationKeys;
description: TranslationKeys; description: TranslationKeys;

View file

@ -4,6 +4,7 @@ const path = require('path');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
const connect = require('./connect'); const connect = require('./connect');
@ -164,9 +165,9 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
await grantPermission({ await grantPermission({
principalType: 'user', principalType: 'user',
principalId: agent.author, principalId: agent.author,
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
accessRoleId: 'agent_owner', accessRoleId: AccessRoleIds.AGENT_OWNER,
grantedBy: agent.author, grantedBy: agent.author,
}); });
results.ownerGrants++; results.ownerGrants++;
@ -178,12 +179,12 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
if (isGlobal) { if (isGlobal) {
if (isCollab) { if (isCollab) {
// Global project + collaborative = Public EDIT access // Global project + collaborative = Public EDIT access
publicRoleId = 'agent_editor'; publicRoleId = AccessRoleIds.AGENT_EDITOR;
description = 'Global Edit'; description = 'Global Edit';
results.publicEditGrants++; results.publicEditGrants++;
} else { } else {
// Global project + not collaborative = Public VIEW access // Global project + not collaborative = Public VIEW access
publicRoleId = 'agent_viewer'; publicRoleId = AccessRoleIds.AGENT_VIEWER;
description = 'Global View'; description = 'Global View';
results.publicViewGrants++; results.publicViewGrants++;
} }
@ -192,7 +193,7 @@ async function migrateAgentPermissionsEnhanced({ dryRun = true, batchSize = 100
await grantPermission({ await grantPermission({
principalType: 'public', principalType: 'public',
principalId: null, principalId: null,
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: agent._id, resourceId: agent._id,
accessRoleId: publicRoleId, accessRoleId: publicRoleId,
grantedBy: agent.author, grantedBy: agent.author,

View file

@ -2,6 +2,7 @@ const path = require('path');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
require('module-alias')({ base: path.resolve(__dirname, '..', 'api') }); require('module-alias')({ base: path.resolve(__dirname, '..', 'api') });
const { AccessRoleIds, ResourceType } = require('librechat-data-provider');
const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants; const { GLOBAL_PROJECT_NAME } = require('librechat-data-provider').Constants;
const connect = require('./connect'); const connect = require('./connect');
@ -146,9 +147,9 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
await grantPermission({ await grantPermission({
principalType: 'user', principalType: 'user',
principalId: group.author, principalId: group.author,
resourceType: 'promptGroup', resourceType: ResourceType.PROMPTGROUP,
resourceId: group._id, resourceId: group._id,
accessRoleId: 'promptGroup_owner', accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: group.author, grantedBy: group.author,
}); });
results.ownerGrants++; results.ownerGrants++;
@ -158,9 +159,9 @@ async function migrateToPromptGroupPermissions({ dryRun = true, batchSize = 100
await grantPermission({ await grantPermission({
principalType: 'public', principalType: 'public',
principalId: null, principalId: null,
resourceType: 'promptGroup', resourceType: ResourceType.PROMPTGROUP,
resourceId: group._id, resourceId: group._id,
accessRoleId: 'promptGroup_viewer', accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
grantedBy: group.author, grantedBy: group.author,
}); });
results.publicViewGrants++; results.publicViewGrants++;

View file

@ -25,26 +25,35 @@ export type TPrincipalSource = 'local' | 'entra';
*/ */
export type TAccessLevel = 'none' | 'viewer' | 'editor' | 'owner'; export type TAccessLevel = 'none' | 'viewer' | 'editor' | 'owner';
/**
* Resource types for permission system
*/
export enum ResourceType {
AGENT = 'agent',
PROMPTGROUP = 'promptGroup',
}
/** /**
* Permission bit constants for bitwise operations * Permission bit constants for bitwise operations
*/ */
export const PERMISSION_BITS = { export enum PermissionBits {
VIEW: 1, // 001 - Can view and use agent /** 001 - Can view and use agent */
EDIT: 2, // 010 - Can modify agent settings VIEW = 1,
DELETE: 4, // 100 - Can delete agent /** 010 - Can modify agent settings */
SHARE: 8, // 1000 - Can share agent with others (future) EDIT = 2,
} as const; /** 100 - Can delete agent */
DELETE = 4,
/** 1000 - Can share agent with others (future) */
SHARE = 8,
}
/** /**
* Standard access role IDs * Standard access role IDs
*/ */
export enum ACCESS_ROLE_IDS { export enum AccessRoleIds {
AGENT_VIEWER = 'agent_viewer', AGENT_VIEWER = 'agent_viewer',
AGENT_EDITOR = 'agent_editor', AGENT_EDITOR = 'agent_editor',
AGENT_OWNER = 'agent_owner', // Future use AGENT_OWNER = 'agent_owner',
PROMPT_VIEWER = 'prompt_viewer',
PROMPT_EDITOR = 'prompt_editor',
PROMPT_OWNER = 'prompt_owner',
PROMPTGROUP_VIEWER = 'promptGroup_viewer', PROMPTGROUP_VIEWER = 'promptGroup_viewer',
PROMPTGROUP_EDITOR = 'promptGroup_editor', PROMPTGROUP_EDITOR = 'promptGroup_editor',
PROMPTGROUP_OWNER = 'promptGroup_owner', PROMPTGROUP_OWNER = 'promptGroup_owner',
@ -64,7 +73,7 @@ export const principalSchema = z.object({
avatar: z.string().optional(), // for user and group types avatar: z.string().optional(), // for user and group types
description: z.string().optional(), // for group type description: z.string().optional(), // for group type
idOnTheSource: z.string().optional(), // Entra ID for users/groups idOnTheSource: z.string().optional(), // Entra ID for users/groups
accessRoleId: z.nativeEnum(ACCESS_ROLE_IDS).optional(), // Access role ID for permissions accessRoleId: z.nativeEnum(AccessRoleIds).optional(), // Access role ID for permissions
memberCount: z.number().optional(), // for group type memberCount: z.number().optional(), // for group type
}); });
@ -72,10 +81,10 @@ export const principalSchema = z.object({
* Access role schema - defines named permission sets * Access role schema - defines named permission sets
*/ */
export const accessRoleSchema = z.object({ export const accessRoleSchema = z.object({
accessRoleId: z.nativeEnum(ACCESS_ROLE_IDS), accessRoleId: z.nativeEnum(AccessRoleIds),
name: z.string(), name: z.string(),
description: z.string().optional(), description: z.string().optional(),
resourceType: z.string().default('agent'), resourceType: z.nativeEnum(ResourceType).default(ResourceType.AGENT),
permBits: z.number(), permBits: z.number(),
}); });
@ -98,7 +107,7 @@ export const permissionEntrySchema = z.object({
* Resource permissions response schema * Resource permissions response schema
*/ */
export const resourcePermissionsResponseSchema = z.object({ export const resourcePermissionsResponseSchema = z.object({
resourceType: z.string(), resourceType: z.nativeEnum(ResourceType),
resourceId: z.string(), resourceId: z.string(),
permissions: z.array(permissionEntrySchema), permissions: z.array(permissionEntrySchema),
}); });
@ -210,7 +219,7 @@ export type TPrincipalSearchResponse = {
* Available roles response * Available roles response
*/ */
export type TAvailableRolesResponse = { export type TAvailableRolesResponse = {
resourceType: string; resourceType: ResourceType;
roles: TAccessRole[]; roles: TAccessRole[];
}; };
@ -219,11 +228,11 @@ export type TAvailableRolesResponse = {
* This matches the enhanced aggregation-based endpoint response format * This matches the enhanced aggregation-based endpoint response format
*/ */
export const getResourcePermissionsResponseSchema = z.object({ export const getResourcePermissionsResponseSchema = z.object({
resourceType: z.string(), resourceType: z.nativeEnum(ResourceType),
resourceId: z.string(), resourceId: z.nativeEnum(AccessRoleIds),
principals: z.array(principalSchema), principals: z.array(principalSchema),
public: z.boolean(), public: z.boolean(),
publicAccessRoleId: z.string().optional(), publicAccessRoleId: z.nativeEnum(AccessRoleIds).optional(),
}); });
/** /**
@ -265,9 +274,9 @@ export interface TPermissionCheck {
* Convert permission bits to access level * Convert permission bits to access level
*/ */
export function permBitsToAccessLevel(permBits: number): TAccessLevel { export function permBitsToAccessLevel(permBits: number): TAccessLevel {
if ((permBits & PERMISSION_BITS.DELETE) > 0) return 'owner'; if ((permBits & PermissionBits.DELETE) > 0) return 'owner';
if ((permBits & PERMISSION_BITS.EDIT) > 0) return 'editor'; if ((permBits & PermissionBits.EDIT) > 0) return 'editor';
if ((permBits & PERMISSION_BITS.VIEW) > 0) return 'viewer'; if ((permBits & PermissionBits.VIEW) > 0) return 'viewer';
return 'none'; return 'none';
} }
@ -276,14 +285,14 @@ export function permBitsToAccessLevel(permBits: number): TAccessLevel {
*/ */
export function accessRoleToPermBits(accessRoleId: string): number { export function accessRoleToPermBits(accessRoleId: string): number {
switch (accessRoleId) { switch (accessRoleId) {
case ACCESS_ROLE_IDS.AGENT_VIEWER: case AccessRoleIds.AGENT_VIEWER:
return PERMISSION_BITS.VIEW; return PermissionBits.VIEW;
case ACCESS_ROLE_IDS.AGENT_EDITOR: case AccessRoleIds.AGENT_EDITOR:
return PERMISSION_BITS.VIEW | PERMISSION_BITS.EDIT; return PermissionBits.VIEW | PermissionBits.EDIT;
case ACCESS_ROLE_IDS.AGENT_OWNER: case AccessRoleIds.AGENT_OWNER:
return PERMISSION_BITS.VIEW | PERMISSION_BITS.EDIT | PERMISSION_BITS.DELETE; return PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE;
default: default:
return PERMISSION_BITS.VIEW; return PermissionBits.VIEW;
} }
} }

View file

@ -1,5 +1,6 @@
import type { AssistantsEndpoint } from './schemas'; import type { AssistantsEndpoint } from './schemas';
import * as q from './types/queries'; import * as q from './types/queries';
import { ResourceType } from './accessPermissions';
// Testing this buildQuery function // Testing this buildQuery function
const buildQuery = (params: Record<string, unknown>): string => { const buildQuery = (params: Record<string, unknown>): string => {
@ -323,15 +324,16 @@ export const searchPrincipals = (params: q.PrincipalSearchParams) => {
return url; return url;
}; };
export const getAccessRoles = (resourceType: string) => `/api/permissions/${resourceType}/roles`; export const getAccessRoles = (resourceType: ResourceType) =>
`/api/permissions/${resourceType}/roles`;
export const getResourcePermissions = (resourceType: string, resourceId: string) => export const getResourcePermissions = (resourceType: ResourceType, resourceId: string) =>
`/api/permissions/${resourceType}/${resourceId}`; `/api/permissions/${resourceType}/${resourceId}`;
export const updateResourcePermissions = (resourceType: string, resourceId: string) => export const updateResourcePermissions = (resourceType: ResourceType, resourceId: string) =>
`/api/permissions/${resourceType}/${resourceId}`; `/api/permissions/${resourceType}/${resourceId}`;
export const getEffectivePermissions = (resourceType: string, resourceId: string) => export const getEffectivePermissions = (resourceType: ResourceType, resourceId: string) =>
`/api/permissions/${resourceType}/${resourceId}/effective`; `/api/permissions/${resourceType}/${resourceId}/effective`;
// SharePoint Graph API Token // SharePoint Graph API Token

View file

@ -909,19 +909,21 @@ export function searchPrincipals(
return request.get(endpoints.searchPrincipals(params)); return request.get(endpoints.searchPrincipals(params));
} }
export function getAccessRoles(resourceType: string): Promise<q.AccessRolesResponse> { export function getAccessRoles(
resourceType: permissions.ResourceType,
): Promise<q.AccessRolesResponse> {
return request.get(endpoints.getAccessRoles(resourceType)); return request.get(endpoints.getAccessRoles(resourceType));
} }
export function getResourcePermissions( export function getResourcePermissions(
resourceType: string, resourceType: permissions.ResourceType,
resourceId: string, resourceId: string,
): Promise<permissions.TGetResourcePermissionsResponse> { ): Promise<permissions.TGetResourcePermissionsResponse> {
return request.get(endpoints.getResourcePermissions(resourceType, resourceId)); return request.get(endpoints.getResourcePermissions(resourceType, resourceId));
} }
export function updateResourcePermissions( export function updateResourcePermissions(
resourceType: string, resourceType: permissions.ResourceType,
resourceId: string, resourceId: string,
data: permissions.TUpdateResourcePermissionsRequest, data: permissions.TUpdateResourcePermissionsRequest,
): Promise<permissions.TUpdateResourcePermissionsResponse> { ): Promise<permissions.TUpdateResourcePermissionsResponse> {
@ -929,7 +931,7 @@ export function updateResourcePermissions(
} }
export function getEffectivePermissions( export function getEffectivePermissions(
resourceType: string, resourceType: permissions.ResourceType,
resourceId: string, resourceId: string,
): Promise<permissions.TEffectivePermissionsResponse> { ): Promise<permissions.TEffectivePermissionsResponse> {
return request.get(endpoints.getEffectivePermissions(resourceType, resourceId)); return request.get(endpoints.getEffectivePermissions(resourceType, resourceId));

View file

@ -14,6 +14,7 @@ import { QueryKeys } from '../keys';
import * as s from '../schemas'; import * as s from '../schemas';
import * as t from '../types'; import * as t from '../types';
import * as permissions from '../accessPermissions'; import * as permissions from '../accessPermissions';
import { ResourceType } from '../accessPermissions';
export { hasPermissions } from '../accessPermissions'; export { hasPermissions } from '../accessPermissions';
@ -405,7 +406,7 @@ export const useSearchPrincipalsQuery = (
}; };
export const useGetAccessRolesQuery = ( export const useGetAccessRolesQuery = (
resourceType: string, resourceType: ResourceType,
config?: UseQueryOptions<q.AccessRolesResponse>, config?: UseQueryOptions<q.AccessRolesResponse>,
): QueryObserverResult<q.AccessRolesResponse> => { ): QueryObserverResult<q.AccessRolesResponse> => {
return useQuery<q.AccessRolesResponse>( return useQuery<q.AccessRolesResponse>(
@ -423,7 +424,7 @@ export const useGetAccessRolesQuery = (
}; };
export const useGetResourcePermissionsQuery = ( export const useGetResourcePermissionsQuery = (
resourceType: string, resourceType: ResourceType,
resourceId: string, resourceId: string,
config?: UseQueryOptions<permissions.TGetResourcePermissionsResponse>, config?: UseQueryOptions<permissions.TGetResourcePermissionsResponse>,
): QueryObserverResult<permissions.TGetResourcePermissionsResponse> => { ): QueryObserverResult<permissions.TGetResourcePermissionsResponse> => {
@ -445,7 +446,7 @@ export const useUpdateResourcePermissionsMutation = (): UseMutationResult<
permissions.TUpdateResourcePermissionsResponse, permissions.TUpdateResourcePermissionsResponse,
Error, Error,
{ {
resourceType: string; resourceType: ResourceType;
resourceId: string; resourceId: string;
data: permissions.TUpdateResourcePermissionsRequest; data: permissions.TUpdateResourcePermissionsRequest;
} }
@ -472,7 +473,7 @@ export const useUpdateResourcePermissionsMutation = (): UseMutationResult<
}; };
export const useGetEffectivePermissionsQuery = ( export const useGetEffectivePermissionsQuery = (
resourceType: string, resourceType: ResourceType,
resourceId: string, resourceId: string,
config?: UseQueryOptions<permissions.TEffectivePermissionsResponse>, config?: UseQueryOptions<permissions.TEffectivePermissionsResponse>,
): QueryObserverResult<permissions.TEffectivePermissionsResponse> => { ): QueryObserverResult<permissions.TEffectivePermissionsResponse> => {

View file

@ -1,5 +1,5 @@
import type { InfiniteData } from '@tanstack/react-query'; import type { InfiniteData } from '@tanstack/react-query';
import type { ACCESS_ROLE_IDS } from '../accessPermissions'; import type { AccessRoleIds } from '../accessPermissions';
import type * as a from '../types/agents'; import type * as a from '../types/agents';
import type * as s from '../schemas'; import type * as s from '../schemas';
import type * as t from '../types'; import type * as t from '../types';
@ -159,7 +159,7 @@ export type PrincipalSearchResponse = {
}; };
export type AccessRole = { export type AccessRole = {
accessRoleId: ACCESS_ROLE_IDS; accessRoleId: AccessRoleIds;
name: string; name: string;
description: string; description: string;
permBits: number; permBits: number;

View file

@ -1,16 +1,4 @@
/** import { PermissionBits } from 'librechat-data-provider';
* Permission bit flags
*/
export enum PermissionBits {
/** 0001 - Can view/access the resource */
VIEW = 1,
/** 0010 - Can modify the resource */
EDIT = 2,
/** 0100 - Can delete the resource */
DELETE = 4,
/** 1000 - Can share the resource with others */
SHARE = 8,
}
/** /**
* Common role combinations * Common role combinations

View file

@ -1,9 +1,10 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { AccessRoleIds, ResourceType, PermissionBits } from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server'; import { MongoMemoryServer } from 'mongodb-memory-server';
import { createAccessRoleMethods } from './accessRole';
import { PermissionBits, RoleBits } from '~/common';
import accessRoleSchema from '~/schema/accessRole';
import type * as t from '~/types'; import type * as t from '~/types';
import { createAccessRoleMethods } from './accessRole';
import accessRoleSchema from '~/schema/accessRole';
import { RoleBits } from '~/common';
let mongoServer: MongoMemoryServer; let mongoServer: MongoMemoryServer;
let AccessRole: mongoose.Model<t.IAccessRole>; let AccessRole: mongoose.Model<t.IAccessRole>;
@ -32,7 +33,7 @@ describe('AccessRole Model Tests', () => {
accessRoleId: 'test_viewer', accessRoleId: 'test_viewer',
name: 'Test Viewer', name: 'Test Viewer',
description: 'Test role for viewer permissions', description: 'Test role for viewer permissions',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, permBits: RoleBits.VIEWER,
}; };
@ -98,7 +99,7 @@ describe('AccessRole Model Tests', () => {
accessRoleId: 'test_editor', accessRoleId: 'test_editor',
name: 'Test Editor', name: 'Test Editor',
description: 'Test role for editor permissions', description: 'Test role for editor permissions',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR, permBits: RoleBits.EDITOR,
}, },
]; ];
@ -120,17 +121,17 @@ describe('AccessRole Model Tests', () => {
// Create sample roles for testing // Create sample roles for testing
await Promise.all([ await Promise.all([
methods.createRole({ methods.createRole({
accessRoleId: 'agent_viewer', accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Agent Viewer', name: 'Agent Viewer',
description: 'Can view agents', description: 'Can view agents',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, permBits: RoleBits.VIEWER,
}), }),
methods.createRole({ methods.createRole({
accessRoleId: 'agent_editor', accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'Agent Editor', name: 'Agent Editor',
description: 'Can edit agents', description: 'Can edit agents',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR, permBits: RoleBits.EDITOR,
}), }),
methods.createRole({ methods.createRole({
@ -154,7 +155,7 @@ describe('AccessRole Model Tests', () => {
const agentRoles = await methods.findRolesByResourceType('agent'); const agentRoles = await methods.findRolesByResourceType('agent');
expect(agentRoles).toHaveLength(2); expect(agentRoles).toHaveLength(2);
expect(agentRoles.map((r) => r.accessRoleId).sort()).toEqual( expect(agentRoles.map((r) => r.accessRoleId).sort()).toEqual(
['agent_editor', 'agent_viewer'].sort(), [AccessRoleIds.AGENT_EDITOR, AccessRoleIds.AGENT_VIEWER].sort(),
); );
const projectRoles = await methods.findRolesByResourceType('project'); const projectRoles = await methods.findRolesByResourceType('project');
@ -167,11 +168,11 @@ describe('AccessRole Model Tests', () => {
test('should find role by permissions', async () => { test('should find role by permissions', async () => {
const viewerRole = await methods.findRoleByPermissions('agent', RoleBits.VIEWER); const viewerRole = await methods.findRoleByPermissions('agent', RoleBits.VIEWER);
expect(viewerRole).toBeDefined(); expect(viewerRole).toBeDefined();
expect(viewerRole?.accessRoleId).toBe('agent_viewer'); expect(viewerRole?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
const editorRole = await methods.findRoleByPermissions('agent', RoleBits.EDITOR); const editorRole = await methods.findRoleByPermissions('agent', RoleBits.EDITOR);
expect(editorRole).toBeDefined(); expect(editorRole).toBeDefined();
expect(editorRole?.accessRoleId).toBe('agent_editor'); expect(editorRole?.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
}); });
test('should return null when no role matches the permissions', async () => { test('should return null when no role matches the permissions', async () => {
@ -192,19 +193,26 @@ describe('AccessRole Model Tests', () => {
// Verify the result contains the default roles // Verify the result contains the default roles
expect(Object.keys(result).sort()).toEqual( expect(Object.keys(result).sort()).toEqual(
['agent_editor', 'agent_owner', 'agent_viewer'].sort(), [
AccessRoleIds.AGENT_EDITOR,
AccessRoleIds.AGENT_OWNER,
AccessRoleIds.AGENT_VIEWER,
AccessRoleIds.PROMPTGROUP_EDITOR,
AccessRoleIds.PROMPTGROUP_OWNER,
AccessRoleIds.PROMPTGROUP_VIEWER,
].sort(),
); );
// Verify each role exists in the database // Verify each role exists in the database
const agentViewerRole = await methods.findRoleByIdentifier('agent_viewer'); const agentViewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(agentViewerRole).toBeDefined(); expect(agentViewerRole).toBeDefined();
expect(agentViewerRole?.permBits).toBe(RoleBits.VIEWER); expect(agentViewerRole?.permBits).toBe(RoleBits.VIEWER);
const agentEditorRole = await methods.findRoleByIdentifier('agent_editor'); const agentEditorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
expect(agentEditorRole).toBeDefined(); expect(agentEditorRole).toBeDefined();
expect(agentEditorRole?.permBits).toBe(RoleBits.EDITOR); expect(agentEditorRole?.permBits).toBe(RoleBits.EDITOR);
const agentOwnerRole = await methods.findRoleByIdentifier('agent_owner'); const agentOwnerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
expect(agentOwnerRole).toBeDefined(); expect(agentOwnerRole).toBeDefined();
expect(agentOwnerRole?.permBits).toBe(RoleBits.OWNER); expect(agentOwnerRole?.permBits).toBe(RoleBits.OWNER);
}); });
@ -212,10 +220,10 @@ describe('AccessRole Model Tests', () => {
test('should not modify existing roles when seeding', async () => { test('should not modify existing roles when seeding', async () => {
// Create a modified version of a default role // Create a modified version of a default role
const customRole = { const customRole = {
accessRoleId: 'agent_viewer', accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Custom Viewer', name: 'Custom Viewer',
description: 'Custom viewer description', description: 'Custom viewer description',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, permBits: RoleBits.VIEWER,
}; };
@ -225,7 +233,7 @@ describe('AccessRole Model Tests', () => {
await methods.seedDefaultRoles(); await methods.seedDefaultRoles();
// Verify the custom role was not modified // Verify the custom role was not modified
const role = await methods.findRoleByIdentifier('agent_viewer'); const role = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(role?.name).toBe(customRole.name); expect(role?.name).toBe(customRole.name);
expect(role?.description).toBe(customRole.description); expect(role?.description).toBe(customRole.description);
}); });
@ -238,27 +246,27 @@ describe('AccessRole Model Tests', () => {
// Create sample roles with ascending permission levels // Create sample roles with ascending permission levels
await Promise.all([ await Promise.all([
methods.createRole({ methods.createRole({
accessRoleId: 'agent_viewer', accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Agent Viewer', name: 'Agent Viewer',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, // 1 permBits: RoleBits.VIEWER, // 1
}), }),
methods.createRole({ methods.createRole({
accessRoleId: 'agent_editor', accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'Agent Editor', name: 'Agent Editor',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR, // 3 permBits: RoleBits.EDITOR, // 3
}), }),
methods.createRole({ methods.createRole({
accessRoleId: 'agent_manager', accessRoleId: 'agent_manager',
name: 'Agent Manager', name: 'Agent Manager',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.MANAGER, // 7 permBits: RoleBits.MANAGER, // 7
}), }),
methods.createRole({ methods.createRole({
accessRoleId: 'agent_owner', accessRoleId: AccessRoleIds.AGENT_OWNER,
name: 'Agent Owner', name: 'Agent Owner',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.OWNER, // 15 permBits: RoleBits.OWNER, // 15
}), }),
]); ]);
@ -267,7 +275,7 @@ describe('AccessRole Model Tests', () => {
test('should find exact matching role', async () => { test('should find exact matching role', async () => {
const role = await methods.getRoleForPermissions('agent', RoleBits.EDITOR); const role = await methods.getRoleForPermissions('agent', RoleBits.EDITOR);
expect(role).toBeDefined(); expect(role).toBeDefined();
expect(role?.accessRoleId).toBe('agent_editor'); expect(role?.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
expect(role?.permBits).toBe(RoleBits.EDITOR); expect(role?.permBits).toBe(RoleBits.EDITOR);
}); });
@ -278,7 +286,7 @@ describe('AccessRole Model Tests', () => {
// Should return VIEWER (1) as closest matching role without exceeding the permission bits // Should return VIEWER (1) as closest matching role without exceeding the permission bits
const role = await methods.getRoleForPermissions('agent', customPerm); const role = await methods.getRoleForPermissions('agent', customPerm);
expect(role).toBeDefined(); expect(role).toBeDefined();
expect(role?.accessRoleId).toBe('agent_viewer'); expect(role?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
}); });
test('should return null when no compatible role is found', async () => { test('should return null when no compatible role is found', async () => {
@ -301,7 +309,7 @@ describe('AccessRole Model Tests', () => {
// Query for agent roles // Query for agent roles
const agentRole = await methods.getRoleForPermissions('agent', RoleBits.VIEWER); const agentRole = await methods.getRoleForPermissions('agent', RoleBits.VIEWER);
expect(agentRole).toBeDefined(); expect(agentRole).toBeDefined();
expect(agentRole?.accessRoleId).toBe('agent_viewer'); expect(agentRole?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
// Query for project roles // Query for project roles
const projectRole = await methods.getRoleForPermissions('project', RoleBits.VIEWER); const projectRole = await methods.getRoleForPermissions('project', RoleBits.VIEWER);

View file

@ -1,6 +1,7 @@
import { AccessRoleIds, ResourceType, PermissionBits } from 'librechat-data-provider';
import type { Model, Types, DeleteResult } from 'mongoose'; import type { Model, Types, DeleteResult } from 'mongoose';
import { RoleBits, PermissionBits } from '~/common';
import type { IAccessRole } from '~/types'; import type { IAccessRole } from '~/types';
import { RoleBits } from '~/common';
export function createAccessRoleMethods(mongoose: typeof import('mongoose')) { export function createAccessRoleMethods(mongoose: typeof import('mongoose')) {
/** /**
@ -104,68 +105,45 @@ export function createAccessRoleMethods(mongoose: typeof import('mongoose')) {
const AccessRole = mongoose.models.AccessRole as Model<IAccessRole>; const AccessRole = mongoose.models.AccessRole as Model<IAccessRole>;
const defaultRoles = [ const defaultRoles = [
{ {
accessRoleId: 'agent_viewer', accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'com_ui_role_viewer', name: 'com_ui_role_viewer',
description: 'com_ui_role_viewer_desc', description: 'com_ui_role_viewer_desc',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, permBits: RoleBits.VIEWER,
}, },
{ {
accessRoleId: 'agent_editor', accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'com_ui_role_editor', name: 'com_ui_role_editor',
description: 'com_ui_role_editor_desc', description: 'com_ui_role_editor_desc',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR, permBits: RoleBits.EDITOR,
}, },
{ {
accessRoleId: 'agent_owner', accessRoleId: AccessRoleIds.AGENT_OWNER,
name: 'com_ui_role_owner', name: 'com_ui_role_owner',
description: 'com_ui_role_owner_desc', description: 'com_ui_role_owner_desc',
resourceType: 'agent', resourceType: ResourceType.AGENT,
permBits: RoleBits.OWNER, permBits: RoleBits.OWNER,
}, },
// Prompt access roles
{ {
accessRoleId: 'prompt_viewer', accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
name: 'com_ui_role_viewer', name: 'com_ui_role_viewer',
description: 'com_ui_role_viewer_desc', description: 'com_ui_role_viewer_desc',
resourceType: 'prompt', resourceType: ResourceType.PROMPTGROUP,
permBits: RoleBits.VIEWER, permBits: RoleBits.VIEWER,
}, },
{ {
accessRoleId: 'prompt_editor', accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
name: 'com_ui_role_editor', name: 'com_ui_role_editor',
description: 'com_ui_role_editor_desc', description: 'com_ui_role_editor_desc',
resourceType: 'prompt', resourceType: ResourceType.PROMPTGROUP,
permBits: RoleBits.EDITOR, permBits: RoleBits.EDITOR,
}, },
{ {
accessRoleId: 'prompt_owner', accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
name: 'com_ui_role_owner', name: 'com_ui_role_owner',
description: 'com_ui_role_owner_desc', description: 'com_ui_role_owner_desc',
resourceType: 'prompt', resourceType: ResourceType.PROMPTGROUP,
permBits: RoleBits.OWNER,
},
// PromptGroup access roles
{
accessRoleId: 'promptGroup_viewer',
name: 'com_ui_role_viewer',
description: 'com_ui_role_viewer_desc',
resourceType: 'promptGroup',
permBits: RoleBits.VIEWER,
},
{
accessRoleId: 'promptGroup_editor',
name: 'com_ui_role_editor',
description: 'com_ui_role_editor_desc',
resourceType: 'promptGroup',
permBits: RoleBits.EDITOR,
},
{
accessRoleId: 'promptGroup_owner',
name: 'com_ui_role_owner',
description: 'com_ui_role_owner_desc',
resourceType: 'promptGroup',
permBits: RoleBits.OWNER, permBits: RoleBits.OWNER,
}, },
]; ];

View file

@ -1,9 +1,9 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { ResourceType, PermissionBits } from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server'; import { MongoMemoryServer } from 'mongodb-memory-server';
import { createAclEntryMethods } from './aclEntry';
import { PermissionBits } from '~/common';
import aclEntrySchema from '~/schema/aclEntry';
import type * as t from '~/types'; import type * as t from '~/types';
import { createAclEntryMethods } from './aclEntry';
import aclEntrySchema from '~/schema/aclEntry';
let mongoServer: MongoMemoryServer; let mongoServer: MongoMemoryServer;
let AclEntry: mongoose.Model<t.IAclEntry>; let AclEntry: mongoose.Model<t.IAclEntry>;
@ -38,7 +38,7 @@ describe('AclEntry Model Tests', () => {
const entry = await methods.grantPermission( const entry = await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -59,7 +59,7 @@ describe('AclEntry Model Tests', () => {
const entry = await methods.grantPermission( const entry = await methods.grantPermission(
'group', 'group',
groupId, groupId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW | PermissionBits.EDIT, PermissionBits.VIEW | PermissionBits.EDIT,
grantedById, grantedById,
@ -76,7 +76,7 @@ describe('AclEntry Model Tests', () => {
const entry = await methods.grantPermission( const entry = await methods.grantPermission(
'public', 'public',
null, null,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -93,7 +93,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -122,7 +122,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -130,7 +130,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'group', 'group',
groupId, groupId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.EDIT, PermissionBits.EDIT,
grantedById, grantedById,
@ -138,7 +138,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'public', 'public',
null, null,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -155,7 +155,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -163,7 +163,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'group', 'group',
groupId, groupId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.EDIT, PermissionBits.EDIT,
grantedById, grantedById,
@ -173,7 +173,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'public', 'public',
null, null,
'agent', ResourceType.AGENT,
otherResourceId, otherResourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -188,7 +188,7 @@ describe('AclEntry Model Tests', () => {
const entries = await methods.findEntriesByPrincipalsAndResource( const entries = await methods.findEntriesByPrincipalsAndResource(
principalsList, principalsList,
'agent', ResourceType.AGENT,
resourceId, resourceId,
); );
expect(entries).toHaveLength(2); expect(entries).toHaveLength(2);
@ -200,7 +200,7 @@ describe('AclEntry Model Tests', () => {
/** User has VIEW permission */ /** User has VIEW permission */
const hasViewPermission = await methods.hasPermission( const hasViewPermission = await methods.hasPermission(
principalsList, principalsList,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
); );
@ -209,7 +209,7 @@ describe('AclEntry Model Tests', () => {
/** User doesn't have EDIT permission */ /** User doesn't have EDIT permission */
const hasEditPermission = await methods.hasPermission( const hasEditPermission = await methods.hasPermission(
principalsList, principalsList,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.EDIT, PermissionBits.EDIT,
); );
@ -222,7 +222,7 @@ describe('AclEntry Model Tests', () => {
/** Group has EDIT permission */ /** Group has EDIT permission */
const hasEditPermission = await methods.hasPermission( const hasEditPermission = await methods.hasPermission(
principalsList, principalsList,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.EDIT, PermissionBits.EDIT,
); );
@ -238,7 +238,7 @@ describe('AclEntry Model Tests', () => {
/** User has VIEW and group has EDIT, together they should have both */ /** User has VIEW and group has EDIT, together they should have both */
const hasViewPermission = await methods.hasPermission( const hasViewPermission = await methods.hasPermission(
principalsList, principalsList,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
); );
@ -246,7 +246,7 @@ describe('AclEntry Model Tests', () => {
const hasEditPermission = await methods.hasPermission( const hasEditPermission = await methods.hasPermission(
principalsList, principalsList,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.EDIT, PermissionBits.EDIT,
); );
@ -255,7 +255,7 @@ describe('AclEntry Model Tests', () => {
/** Neither has DELETE permission */ /** Neither has DELETE permission */
const hasDeletePermission = await methods.hasPermission( const hasDeletePermission = await methods.hasPermission(
principalsList, principalsList,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.DELETE, PermissionBits.DELETE,
); );
@ -281,7 +281,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -292,7 +292,7 @@ describe('AclEntry Model Tests', () => {
expect(entriesBefore).toHaveLength(1); expect(entriesBefore).toHaveLength(1);
/** Revoke it */ /** Revoke it */
const result = await methods.revokePermission('user', userId, 'agent', resourceId); const result = await methods.revokePermission('user', userId, ResourceType.AGENT, resourceId);
expect(result.deletedCount).toBe(1); expect(result.deletedCount).toBe(1);
/** Verify it's gone */ /** Verify it's gone */
@ -305,7 +305,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -315,7 +315,7 @@ describe('AclEntry Model Tests', () => {
const updated = await methods.modifyPermissionBits( const updated = await methods.modifyPermissionBits(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.EDIT, PermissionBits.EDIT,
null, null,
@ -330,7 +330,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW | PermissionBits.EDIT, PermissionBits.VIEW | PermissionBits.EDIT,
grantedById, grantedById,
@ -340,7 +340,7 @@ describe('AclEntry Model Tests', () => {
const updated = await methods.modifyPermissionBits( const updated = await methods.modifyPermissionBits(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
null, null,
PermissionBits.EDIT, PermissionBits.EDIT,
@ -355,7 +355,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId, resourceId,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -387,7 +387,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId1, resourceId1,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -397,7 +397,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'user', 'user',
userId, userId,
'agent', ResourceType.AGENT,
resourceId2, resourceId2,
PermissionBits.VIEW | PermissionBits.EDIT, PermissionBits.VIEW | PermissionBits.EDIT,
grantedById, grantedById,
@ -407,7 +407,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission( await methods.grantPermission(
'group', 'group',
groupId, groupId,
'agent', ResourceType.AGENT,
resourceId3, resourceId3,
PermissionBits.VIEW, PermissionBits.VIEW,
grantedById, grantedById,
@ -416,7 +416,7 @@ describe('AclEntry Model Tests', () => {
/** Find resources with VIEW permission for user */ /** Find resources with VIEW permission for user */
const userViewableResources = await methods.findAccessibleResources( const userViewableResources = await methods.findAccessibleResources(
[{ principalType: 'user', principalId: userId }], [{ principalType: 'user', principalId: userId }],
'agent', ResourceType.AGENT,
PermissionBits.VIEW, PermissionBits.VIEW,
); );
@ -431,7 +431,7 @@ describe('AclEntry Model Tests', () => {
{ principalType: 'user', principalId: userId }, { principalType: 'user', principalId: userId },
{ principalType: 'group', principalId: groupId }, { principalType: 'group', principalId: groupId },
], ],
'agent', ResourceType.AGENT,
PermissionBits.VIEW, PermissionBits.VIEW,
); );
@ -440,7 +440,7 @@ describe('AclEntry Model Tests', () => {
/** Find resources with EDIT permission for user */ /** Find resources with EDIT permission for user */
const editableResources = await methods.findAccessibleResources( const editableResources = await methods.findAccessibleResources(
[{ principalType: 'user', principalId: userId }], [{ principalType: 'user', principalId: userId }],
'agent', ResourceType.AGENT,
PermissionBits.EDIT, PermissionBits.EDIT,
); );
@ -467,7 +467,7 @@ describe('AclEntry Model Tests', () => {
principalType: 'user', principalType: 'user',
principalId: userId, principalId: userId,
principalModel: 'User', principalModel: 'User',
resourceType: 'agent', resourceType: ResourceType.AGENT,
resourceId: childResourceId, resourceId: childResourceId,
permBits: PermissionBits.VIEW, permBits: PermissionBits.VIEW,
grantedBy: grantedById, grantedBy: grantedById,
@ -477,7 +477,7 @@ describe('AclEntry Model Tests', () => {
/** Get effective permissions */ /** Get effective permissions */
const effective = await methods.getEffectivePermissions( const effective = await methods.getEffectivePermissions(
[{ principalType: 'user', principalId: userId }], [{ principalType: 'user', principalId: userId }],
'agent', ResourceType.AGENT,
childResourceId, childResourceId,
); );

View file

@ -16,7 +16,7 @@ const accessRoleSchema = new Schema<IAccessRole>(
description: String, description: String,
resourceType: { resourceType: {
type: String, type: String,
enum: ['agent', 'project', 'file', 'prompt', 'promptGroup'], enum: ['agent', 'project', 'file', 'promptGroup'],
required: true, required: true,
default: 'agent', default: 'agent',
}, },

View file

@ -7,8 +7,8 @@ export type AclEntry = {
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?: 'User' | 'Group';
/** The type of resource ('agent', 'project', 'file', 'prompt', 'promptGroup') */ /** The type of resource ('agent', 'project', 'file', 'promptGroup') */
resourceType: 'agent' | 'project' | 'file' | 'prompt' | 'promptGroup'; resourceType: 'agent' | 'project' | 'file' | 'promptGroup';
/** 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 */