From 0551a562d80ddb6430a4935b71b067bd9ad938a2 Mon Sep 17 00:00:00 2001 From: Ruben Talstra Date: Sat, 5 Apr 2025 01:47:14 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=BA=20refactor:=20Nest=20Permission=20?= =?UTF-8?q?fields=20for=20Roles=20(#6487)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🏗️ feat: Add Group model and schema with GroupType enum * 🏗️ feat: Introduce Permissions module and refactor role-based access control * 🏗️ feat: Refactor permissions handling and consolidate permission schemas * 🏗️ feat: Refactor role permissions handling and improve role initialization logic * 🏗️ feat: Update Role.spec.js to improve imports and enhance test structure * 🏗️ feat: Update access control logic to ensure proper permission checks in role handling * 🏗️ chore: Bump versions for librechat-data-provider to 0.7.75 and @librechat/data-schemas to 0.0.6 * 🏗️ feat: Improve role permissions handling by ensuring defaults are applied correctly * 🏗️ feat: Update role permissions schema to comment out unused SHARE permission * 🏗️ chore: Bump version of librechat-data-provider to 0.7.77 and remove unused groups field from IUser interface * 🏗️ chore: Downgrade version of librechat-data-provider to 0.7.76 * 🔧 chore: Bump versions for librechat-data-provider to 0.7.77 and data-schemas to 0.0.6 * 🏗️ chore: Update version of librechat-data-provider to 0.7.789 --------- Co-authored-by: Danny Avila --- api/models/Role.js | 71 ++--- api/models/Role.spec.js | 297 +++++++++--------- .../middleware/roles/generateCheckAccess.js | 4 +- client/src/hooks/Roles/useHasAccess.ts | 2 +- package-lock.json | 2 +- packages/data-provider/src/index.ts | 1 + packages/data-provider/src/permissions.ts | 90 ++++++ packages/data-provider/src/roles.ts | 182 ++++------- packages/data-provider/src/types/mutations.ts | 5 +- packages/data-schemas/package.json | 2 +- packages/data-schemas/src/schema/role.ts | 149 +++++---- 11 files changed, 394 insertions(+), 411 deletions(-) create mode 100644 packages/data-provider/src/permissions.ts diff --git a/api/models/Role.js b/api/models/Role.js index 4be5faead..c4abfedad 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -4,13 +4,8 @@ const { SystemRoles, roleDefaults, PermissionTypes, + permissionsSchema, removeNullishValues, - agentPermissionsSchema, - promptPermissionsSchema, - runCodePermissionsSchema, - bookmarkPermissionsSchema, - multiConvoPermissionsSchema, - temporaryChatPermissionsSchema, } = require('librechat-data-provider'); const getLogStores = require('~/cache/getLogStores'); const { roleSchema } = require('@librechat/data-schemas'); @@ -20,15 +15,16 @@ const Role = mongoose.model('Role', roleSchema); /** * Retrieve a role by name and convert the found role document to a plain object. - * If the role with the given name doesn't exist and the name is a system defined role, create it and return the lean version. + * If the role with the given name doesn't exist and the name is a system defined role, + * create it and return the lean version. * * @param {string} roleName - The name of the role to find or create. * @param {string|string[]} [fieldsToSelect] - The fields to include or exclude in the returned document. * @returns {Promise} A plain object representing the role document. */ const getRoleByName = async function (roleName, fieldsToSelect = null) { + const cache = getLogStores(CacheKeys.ROLES); try { - const cache = getLogStores(CacheKeys.ROLES); const cachedRole = await cache.get(roleName); if (cachedRole) { return cachedRole; @@ -40,8 +36,7 @@ const getRoleByName = async function (roleName, fieldsToSelect = null) { let role = await query.lean().exec(); if (!role && SystemRoles[roleName]) { - role = roleDefaults[roleName]; - role = await new Role(role).save(); + role = await new Role(roleDefaults[roleName]).save(); await cache.set(roleName, role); return role.toObject(); } @@ -60,8 +55,8 @@ const getRoleByName = async function (roleName, fieldsToSelect = null) { * @returns {Promise} Updated role document. */ const updateRoleByName = async function (roleName, updates) { + const cache = getLogStores(CacheKeys.ROLES); try { - const cache = getLogStores(CacheKeys.ROLES); const role = await Role.findOneAndUpdate( { name: roleName }, { $set: updates }, @@ -77,29 +72,20 @@ const updateRoleByName = async function (roleName, updates) { } }; -const permissionSchemas = { - [PermissionTypes.AGENTS]: agentPermissionsSchema, - [PermissionTypes.PROMPTS]: promptPermissionsSchema, - [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, - [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, - [PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema, - [PermissionTypes.RUN_CODE]: runCodePermissionsSchema, -}; - /** * Updates access permissions for a specific role and multiple permission types. - * @param {SystemRoles} roleName - The role to update. + * @param {string} roleName - The role to update. * @param {Object.>} permissionsUpdate - Permissions to update and their values. */ async function updateAccessPermissions(roleName, permissionsUpdate) { + // Filter and clean the permission updates based on our schema definition. const updates = {}; for (const [permissionType, permissions] of Object.entries(permissionsUpdate)) { - if (permissionSchemas[permissionType]) { + if (permissionsSchema.shape && permissionsSchema.shape[permissionType]) { updates[permissionType] = removeNullishValues(permissions); } } - - if (Object.keys(updates).length === 0) { + if (!Object.keys(updates).length) { return; } @@ -109,26 +95,28 @@ async function updateAccessPermissions(roleName, permissionsUpdate) { return; } - const updatedPermissions = {}; + const currentPermissions = role.permissions || {}; + const updatedPermissions = { ...currentPermissions }; let hasChanges = false; for (const [permissionType, permissions] of Object.entries(updates)) { - const currentPermissions = role[permissionType] || {}; - updatedPermissions[permissionType] = { ...currentPermissions }; + const currentTypePermissions = currentPermissions[permissionType] || {}; + updatedPermissions[permissionType] = { ...currentTypePermissions }; for (const [permission, value] of Object.entries(permissions)) { - if (currentPermissions[permission] !== value) { + if (currentTypePermissions[permission] !== value) { updatedPermissions[permissionType][permission] = value; hasChanges = true; logger.info( - `Updating '${roleName}' role ${permissionType} '${permission}' permission from ${currentPermissions[permission]} to: ${value}`, + `Updating '${roleName}' role permission '${permissionType}' '${permission}' from ${currentTypePermissions[permission]} to: ${value}`, ); } } } if (hasChanges) { - await updateRoleByName(roleName, updatedPermissions); + // Update only the permissions field. + await updateRoleByName(roleName, { permissions: updatedPermissions }); logger.info(`Updated '${roleName}' role permissions`); } else { logger.info(`No changes needed for '${roleName}' role permissions`); @@ -146,30 +134,27 @@ async function updateAccessPermissions(roleName, permissionsUpdate) { * @returns {Promise} */ const initializeRoles = async function () { - const defaultRoles = [SystemRoles.ADMIN, SystemRoles.USER]; - - for (const roleName of defaultRoles) { + for (const roleName of [SystemRoles.ADMIN, SystemRoles.USER]) { let role = await Role.findOne({ name: roleName }); + const defaultPerms = roleDefaults[roleName].permissions; if (!role) { - // Create new role if it doesn't exist + // Create new role if it doesn't exist. role = new Role(roleDefaults[roleName]); } else { - // Add missing permission types - let isUpdated = false; - for (const permType of Object.values(PermissionTypes)) { - if (!role[permType]) { - role[permType] = roleDefaults[roleName][permType]; - isUpdated = true; + // Ensure role.permissions is defined. + role.permissions = role.permissions || {}; + // For each permission type in defaults, add it if missing. + for (const permType of Object.keys(defaultPerms)) { + if (role.permissions[permType] == null) { + role.permissions[permType] = defaultPerms[permType]; } } - if (isUpdated) { - await role.save(); - } } await role.save(); } }; + module.exports = { Role, getRoleByName, diff --git a/api/models/Role.spec.js b/api/models/Role.spec.js index 39611f7b9..a8b60801c 100644 --- a/api/models/Role.spec.js +++ b/api/models/Role.spec.js @@ -2,22 +2,21 @@ const mongoose = require('mongoose'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { SystemRoles, - PermissionTypes, - roleDefaults, Permissions, + roleDefaults, + PermissionTypes, } = require('librechat-data-provider'); -const { updateAccessPermissions, initializeRoles } = require('~/models/Role'); +const { Role, getRoleByName, updateAccessPermissions, initializeRoles } = require('~/models/Role'); const getLogStores = require('~/cache/getLogStores'); -const { Role } = require('~/models/Role'); // Mock the cache -jest.mock('~/cache/getLogStores', () => { - return jest.fn().mockReturnValue({ +jest.mock('~/cache/getLogStores', () => + jest.fn().mockReturnValue({ get: jest.fn(), set: jest.fn(), del: jest.fn(), - }); -}); + }), +); let mongoServer; @@ -41,10 +40,12 @@ describe('updateAccessPermissions', () => { it('should update permissions when changes are needed', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - CREATE: true, - USE: true, - SHARED_GLOBAL: false, + permissions: { + [PermissionTypes.PROMPTS]: { + CREATE: true, + USE: true, + SHARED_GLOBAL: false, + }, }, }).save(); @@ -56,8 +57,8 @@ describe('updateAccessPermissions', () => { }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({ + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, SHARED_GLOBAL: true, @@ -67,10 +68,12 @@ describe('updateAccessPermissions', () => { it('should not update permissions when no changes are needed', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - CREATE: true, - USE: true, - SHARED_GLOBAL: false, + permissions: { + [PermissionTypes.PROMPTS]: { + CREATE: true, + USE: true, + SHARED_GLOBAL: false, + }, }, }).save(); @@ -82,8 +85,8 @@ describe('updateAccessPermissions', () => { }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({ + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, SHARED_GLOBAL: false, @@ -92,11 +95,8 @@ describe('updateAccessPermissions', () => { it('should handle non-existent roles', async () => { await updateAccessPermissions('NON_EXISTENT_ROLE', { - [PermissionTypes.PROMPTS]: { - CREATE: true, - }, + [PermissionTypes.PROMPTS]: { CREATE: true }, }); - const role = await Role.findOne({ name: 'NON_EXISTENT_ROLE' }); expect(role).toBeNull(); }); @@ -104,21 +104,21 @@ describe('updateAccessPermissions', () => { it('should update only specified permissions', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - CREATE: true, - USE: true, - SHARED_GLOBAL: false, + permissions: { + [PermissionTypes.PROMPTS]: { + CREATE: true, + USE: true, + SHARED_GLOBAL: false, + }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { - SHARED_GLOBAL: true, - }, + [PermissionTypes.PROMPTS]: { SHARED_GLOBAL: true }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({ + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, SHARED_GLOBAL: true, @@ -128,21 +128,21 @@ describe('updateAccessPermissions', () => { it('should handle partial updates', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - CREATE: true, - USE: true, - SHARED_GLOBAL: false, + permissions: { + [PermissionTypes.PROMPTS]: { + CREATE: true, + USE: true, + SHARED_GLOBAL: false, + }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.PROMPTS]: { - USE: false, - }, + [PermissionTypes.PROMPTS]: { USE: false }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({ + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: false, SHARED_GLOBAL: false, @@ -152,13 +152,9 @@ describe('updateAccessPermissions', () => { it('should update multiple permission types at once', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - CREATE: true, - USE: true, - SHARED_GLOBAL: false, - }, - [PermissionTypes.BOOKMARKS]: { - USE: true, + permissions: { + [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARED_GLOBAL: false }, + [PermissionTypes.BOOKMARKS]: { USE: true }, }, }).save(); @@ -167,24 +163,20 @@ describe('updateAccessPermissions', () => { [PermissionTypes.BOOKMARKS]: { USE: false }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({ + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: false, SHARED_GLOBAL: true, }); - expect(updatedRole[PermissionTypes.BOOKMARKS]).toEqual({ - USE: false, - }); + expect(updatedRole.permissions[PermissionTypes.BOOKMARKS]).toEqual({ USE: false }); }); it('should handle updates for a single permission type', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - CREATE: true, - USE: true, - SHARED_GLOBAL: false, + permissions: { + [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARED_GLOBAL: false }, }, }).save(); @@ -192,8 +184,8 @@ describe('updateAccessPermissions', () => { [PermissionTypes.PROMPTS]: { USE: false, SHARED_GLOBAL: true }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({ + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: false, SHARED_GLOBAL: true, @@ -203,33 +195,25 @@ describe('updateAccessPermissions', () => { it('should update MULTI_CONVO permissions', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.MULTI_CONVO]: { - USE: false, + permissions: { + [PermissionTypes.MULTI_CONVO]: { USE: false }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.MULTI_CONVO]: { - USE: true, - }, + [PermissionTypes.MULTI_CONVO]: { USE: true }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({ - USE: true, - }); + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]).toEqual({ USE: true }); }); it('should update MULTI_CONVO permissions along with other permission types', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - CREATE: true, - USE: true, - SHARED_GLOBAL: false, - }, - [PermissionTypes.MULTI_CONVO]: { - USE: false, + permissions: { + [PermissionTypes.PROMPTS]: { CREATE: true, USE: true, SHARED_GLOBAL: false }, + [PermissionTypes.MULTI_CONVO]: { USE: false }, }, }).save(); @@ -238,35 +222,29 @@ describe('updateAccessPermissions', () => { [PermissionTypes.MULTI_CONVO]: { USE: true }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({ + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.PROMPTS]).toEqual({ CREATE: true, USE: true, SHARED_GLOBAL: true, }); - expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({ - USE: true, - }); + expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]).toEqual({ USE: true }); }); it('should not update MULTI_CONVO permissions when no changes are needed', async () => { await new Role({ name: SystemRoles.USER, - [PermissionTypes.MULTI_CONVO]: { - USE: true, + permissions: { + [PermissionTypes.MULTI_CONVO]: { USE: true }, }, }).save(); await updateAccessPermissions(SystemRoles.USER, { - [PermissionTypes.MULTI_CONVO]: { - USE: true, - }, + [PermissionTypes.MULTI_CONVO]: { USE: true }, }); - const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - expect(updatedRole[PermissionTypes.MULTI_CONVO]).toEqual({ - USE: true, - }); + const updatedRole = await getRoleByName(SystemRoles.USER); + expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]).toEqual({ USE: true }); }); }); @@ -278,65 +256,69 @@ describe('initializeRoles', () => { it('should create default roles if they do not exist', async () => { await initializeRoles(); - const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean(); - const userRole = await Role.findOne({ name: SystemRoles.USER }).lean(); + const adminRole = await getRoleByName(SystemRoles.ADMIN); + const userRole = await getRoleByName(SystemRoles.USER); expect(adminRole).toBeTruthy(); expect(userRole).toBeTruthy(); - // Check if all permission types exist + // Check if all permission types exist in the permissions field Object.values(PermissionTypes).forEach((permType) => { - expect(adminRole[permType]).toBeDefined(); - expect(userRole[permType]).toBeDefined(); + expect(adminRole.permissions[permType]).toBeDefined(); + expect(userRole.permissions[permType]).toBeDefined(); }); - // Check if permissions match defaults (example for ADMIN role) - expect(adminRole[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBe(true); - expect(adminRole[PermissionTypes.BOOKMARKS].USE).toBe(true); - expect(adminRole[PermissionTypes.AGENTS].CREATE).toBe(true); + // Example: Check default values for ADMIN role + expect(adminRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBe(true); + expect(adminRole.permissions[PermissionTypes.BOOKMARKS].USE).toBe(true); + expect(adminRole.permissions[PermissionTypes.AGENTS].CREATE).toBe(true); }); it('should not modify existing permissions for existing roles', async () => { const customUserRole = { name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: { - [Permissions.USE]: false, - [Permissions.CREATE]: true, - [Permissions.SHARED_GLOBAL]: true, - }, - [PermissionTypes.BOOKMARKS]: { - [Permissions.USE]: false, + permissions: { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: true, + [Permissions.SHARED_GLOBAL]: true, + }, + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: false }, }, }; await new Role(customUserRole).save(); - await initializeRoles(); - const userRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - - expect(userRole[PermissionTypes.PROMPTS]).toEqual(customUserRole[PermissionTypes.PROMPTS]); - expect(userRole[PermissionTypes.BOOKMARKS]).toEqual(customUserRole[PermissionTypes.BOOKMARKS]); - expect(userRole[PermissionTypes.AGENTS]).toBeDefined(); + const userRole = await getRoleByName(SystemRoles.USER); + expect(userRole.permissions[PermissionTypes.PROMPTS]).toEqual( + customUserRole.permissions[PermissionTypes.PROMPTS], + ); + expect(userRole.permissions[PermissionTypes.BOOKMARKS]).toEqual( + customUserRole.permissions[PermissionTypes.BOOKMARKS], + ); + expect(userRole.permissions[PermissionTypes.AGENTS]).toBeDefined(); }); it('should add new permission types to existing roles', async () => { const partialUserRole = { name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: roleDefaults[SystemRoles.USER][PermissionTypes.PROMPTS], - [PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.USER][PermissionTypes.BOOKMARKS], + permissions: { + [PermissionTypes.PROMPTS]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS], + [PermissionTypes.BOOKMARKS]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.BOOKMARKS], + }, }; await new Role(partialUserRole).save(); - await initializeRoles(); - const userRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - - expect(userRole[PermissionTypes.AGENTS]).toBeDefined(); - expect(userRole[PermissionTypes.AGENTS].CREATE).toBeDefined(); - expect(userRole[PermissionTypes.AGENTS].USE).toBeDefined(); - expect(userRole[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined(); + const userRole = await getRoleByName(SystemRoles.USER); + expect(userRole.permissions[PermissionTypes.AGENTS]).toBeDefined(); + expect(userRole.permissions[PermissionTypes.AGENTS].CREATE).toBeDefined(); + expect(userRole.permissions[PermissionTypes.AGENTS].USE).toBeDefined(); + expect(userRole.permissions[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined(); }); it('should handle multiple runs without duplicating or modifying data', async () => { @@ -349,72 +331,73 @@ describe('initializeRoles', () => { expect(adminRoles).toHaveLength(1); expect(userRoles).toHaveLength(1); - const adminRole = adminRoles[0].toObject(); - const userRole = userRoles[0].toObject(); - - // Check if all permission types exist + const adminPerms = adminRoles[0].toObject().permissions; + const userPerms = userRoles[0].toObject().permissions; Object.values(PermissionTypes).forEach((permType) => { - expect(adminRole[permType]).toBeDefined(); - expect(userRole[permType]).toBeDefined(); + expect(adminPerms[permType]).toBeDefined(); + expect(userPerms[permType]).toBeDefined(); }); }); it('should update roles with missing permission types from roleDefaults', async () => { const partialAdminRole = { name: SystemRoles.ADMIN, - [PermissionTypes.PROMPTS]: { - [Permissions.USE]: false, - [Permissions.CREATE]: false, - [Permissions.SHARED_GLOBAL]: false, + permissions: { + [PermissionTypes.PROMPTS]: { + [Permissions.USE]: false, + [Permissions.CREATE]: false, + [Permissions.SHARED_GLOBAL]: false, + }, + [PermissionTypes.BOOKMARKS]: + roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.BOOKMARKS], }, - [PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.ADMIN][PermissionTypes.BOOKMARKS], }; await new Role(partialAdminRole).save(); - await initializeRoles(); - const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean(); - - expect(adminRole[PermissionTypes.PROMPTS]).toEqual(partialAdminRole[PermissionTypes.PROMPTS]); - expect(adminRole[PermissionTypes.AGENTS]).toBeDefined(); - expect(adminRole[PermissionTypes.AGENTS].CREATE).toBeDefined(); - expect(adminRole[PermissionTypes.AGENTS].USE).toBeDefined(); - expect(adminRole[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined(); + const adminRole = await getRoleByName(SystemRoles.ADMIN); + expect(adminRole.permissions[PermissionTypes.PROMPTS]).toEqual( + partialAdminRole.permissions[PermissionTypes.PROMPTS], + ); + expect(adminRole.permissions[PermissionTypes.AGENTS]).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.AGENTS].CREATE).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.AGENTS].USE).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeDefined(); }); it('should include MULTI_CONVO permissions when creating default roles', async () => { await initializeRoles(); - const adminRole = await Role.findOne({ name: SystemRoles.ADMIN }).lean(); - const userRole = await Role.findOne({ name: SystemRoles.USER }).lean(); + const adminRole = await getRoleByName(SystemRoles.ADMIN); + const userRole = await getRoleByName(SystemRoles.USER); - expect(adminRole[PermissionTypes.MULTI_CONVO]).toBeDefined(); - expect(userRole[PermissionTypes.MULTI_CONVO]).toBeDefined(); - - // Check if MULTI_CONVO permissions match defaults - expect(adminRole[PermissionTypes.MULTI_CONVO].USE).toBe( - roleDefaults[SystemRoles.ADMIN][PermissionTypes.MULTI_CONVO].USE, + expect(adminRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined(); + expect(userRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined(); + expect(adminRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe( + roleDefaults[SystemRoles.ADMIN].permissions[PermissionTypes.MULTI_CONVO].USE, ); - expect(userRole[PermissionTypes.MULTI_CONVO].USE).toBe( - roleDefaults[SystemRoles.USER][PermissionTypes.MULTI_CONVO].USE, + expect(userRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe( + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.MULTI_CONVO].USE, ); }); it('should add MULTI_CONVO permissions to existing roles without them', async () => { const partialUserRole = { name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: roleDefaults[SystemRoles.USER][PermissionTypes.PROMPTS], - [PermissionTypes.BOOKMARKS]: roleDefaults[SystemRoles.USER][PermissionTypes.BOOKMARKS], + permissions: { + [PermissionTypes.PROMPTS]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.PROMPTS], + [PermissionTypes.BOOKMARKS]: + roleDefaults[SystemRoles.USER].permissions[PermissionTypes.BOOKMARKS], + }, }; await new Role(partialUserRole).save(); - await initializeRoles(); - const userRole = await Role.findOne({ name: SystemRoles.USER }).lean(); - - expect(userRole[PermissionTypes.MULTI_CONVO]).toBeDefined(); - expect(userRole[PermissionTypes.MULTI_CONVO].USE).toBeDefined(); + const userRole = await getRoleByName(SystemRoles.USER); + expect(userRole.permissions[PermissionTypes.MULTI_CONVO]).toBeDefined(); + expect(userRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBeDefined(); }); }); diff --git a/api/server/middleware/roles/generateCheckAccess.js b/api/server/middleware/roles/generateCheckAccess.js index 0f137c3c8..cabbd405b 100644 --- a/api/server/middleware/roles/generateCheckAccess.js +++ b/api/server/middleware/roles/generateCheckAccess.js @@ -17,9 +17,9 @@ const checkAccess = async (user, permissionType, permissions, bodyProps = {}, ch } const role = await getRoleByName(user.role); - if (role && role[permissionType]) { + if (role && role.permissions && role.permissions[permissionType]) { const hasAnyPermission = permissions.some((permission) => { - if (role[permissionType][permission]) { + if (role.permissions[permissionType][permission]) { return true; } diff --git a/client/src/hooks/Roles/useHasAccess.ts b/client/src/hooks/Roles/useHasAccess.ts index 4cb8ab38e..649faeb62 100644 --- a/client/src/hooks/Roles/useHasAccess.ts +++ b/client/src/hooks/Roles/useHasAccess.ts @@ -29,7 +29,7 @@ const useHasAccess = ({ } if (isAuthenticated && user?.role != null && roles && roles[user.role]) { - return roles[user.role]?.[permissionType]?.[permission] === true; + return roles[user.role]?.permissions?.[permissionType]?.[permission] === true; } return false; }, diff --git a/package-lock.json b/package-lock.json index 911cd7f72..599184d81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43845,7 +43845,7 @@ }, "packages/data-schemas": { "name": "@librechat/data-schemas", - "version": "0.0.5", + "version": "0.0.6", "license": "MIT", "dependencies": { "mongoose": "^8.12.1" diff --git a/packages/data-provider/src/index.ts b/packages/data-provider/src/index.ts index 028ed07f1..849062a9c 100644 --- a/packages/data-provider/src/index.ts +++ b/packages/data-provider/src/index.ts @@ -15,6 +15,7 @@ export * from './models'; /* mcp */ export * from './mcp'; /* RBAC */ +export * from './permissions'; export * from './roles'; /* types (exports schemas from `./types` as they contain needed in other defs) */ export * from './types'; diff --git a/packages/data-provider/src/permissions.ts b/packages/data-provider/src/permissions.ts new file mode 100644 index 000000000..ea78d449b --- /dev/null +++ b/packages/data-provider/src/permissions.ts @@ -0,0 +1,90 @@ +import { z } from 'zod'; + +/** + * Enum for Permission Types + */ +export enum PermissionTypes { + /** + * Type for Prompt Permissions + */ + PROMPTS = 'PROMPTS', + /** + * Type for Bookmark Permissions + */ + BOOKMARKS = 'BOOKMARKS', + /** + * Type for Agent Permissions + */ + AGENTS = 'AGENTS', + /** + * Type for Multi-Conversation Permissions + */ + MULTI_CONVO = 'MULTI_CONVO', + /** + * Type for Temporary Chat + */ + TEMPORARY_CHAT = 'TEMPORARY_CHAT', + /** + * Type for using the "Run Code" LC Code Interpreter API feature + */ + RUN_CODE = 'RUN_CODE', +} + +/** + * Enum for Role-Based Access Control Constants + */ +export enum Permissions { + SHARED_GLOBAL = 'SHARED_GLOBAL', + USE = 'USE', + CREATE = 'CREATE', + UPDATE = 'UPDATE', + READ = 'READ', + READ_AUTHOR = 'READ_AUTHOR', + SHARE = 'SHARE', +} + +export const promptPermissionsSchema = z.object({ + [Permissions.SHARED_GLOBAL]: z.boolean().default(false), + [Permissions.USE]: z.boolean().default(true), + [Permissions.CREATE]: z.boolean().default(true), + // [Permissions.SHARE]: z.boolean().default(false), +}); +export type TPromptPermissions = z.infer; + +export const bookmarkPermissionsSchema = z.object({ + [Permissions.USE]: z.boolean().default(true), +}); +export type TBookmarkPermissions = z.infer; + +export const agentPermissionsSchema = z.object({ + [Permissions.SHARED_GLOBAL]: z.boolean().default(false), + [Permissions.USE]: z.boolean().default(true), + [Permissions.CREATE]: z.boolean().default(true), + // [Permissions.SHARE]: z.boolean().default(false), +}); +export type TAgentPermissions = z.infer; + +export const multiConvoPermissionsSchema = z.object({ + [Permissions.USE]: z.boolean().default(true), +}); +export type TMultiConvoPermissions = z.infer; + +export const temporaryChatPermissionsSchema = z.object({ + [Permissions.USE]: z.boolean().default(true), +}); +export type TTemporaryChatPermissions = z.infer; + +export const runCodePermissionsSchema = z.object({ + [Permissions.USE]: z.boolean().default(true), +}); +export type TRunCodePermissions = z.infer; + +// Define a single permissions schema that holds all permission types. +export const permissionsSchema = z.object({ + [PermissionTypes.PROMPTS]: promptPermissionsSchema, + [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, + [PermissionTypes.AGENTS]: agentPermissionsSchema, + [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, + [PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema, + [PermissionTypes.RUN_CODE]: runCodePermissionsSchema, +}); diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index b7a8752e8..ec863fd94 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -1,4 +1,15 @@ import { z } from 'zod'; +import { + Permissions, + PermissionTypes, + permissionsSchema, + agentPermissionsSchema, + promptPermissionsSchema, + runCodePermissionsSchema, + bookmarkPermissionsSchema, + multiConvoPermissionsSchema, + temporaryChatPermissionsSchema, +} from './permissions'; /** * Enum for System Defined Roles @@ -14,153 +25,72 @@ export enum SystemRoles { USER = 'USER', } -/** - * Enum for Permission Types - */ -export enum PermissionTypes { - /** - * Type for Prompt Permissions - */ - PROMPTS = 'PROMPTS', - /** - * Type for Bookmark Permissions - */ - BOOKMARKS = 'BOOKMARKS', - /** - * Type for Agent Permissions - */ - AGENTS = 'AGENTS', - /** - * Type for Multi-Conversation Permissions - */ - MULTI_CONVO = 'MULTI_CONVO', - /** - * Type for Temporary Chat - */ - TEMPORARY_CHAT = 'TEMPORARY_CHAT', - /** - * Type for using the "Run Code" LC Code Interpreter API feature - */ - RUN_CODE = 'RUN_CODE', -} - -/** - * Enum for Role-Based Access Control Constants - */ -export enum Permissions { - SHARED_GLOBAL = 'SHARED_GLOBAL', - USE = 'USE', - CREATE = 'CREATE', - UPDATE = 'UPDATE', - READ = 'READ', - READ_AUTHOR = 'READ_AUTHOR', - SHARE = 'SHARE', -} - -export const promptPermissionsSchema = z.object({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(false), - [Permissions.USE]: z.boolean().default(true), - [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(false), -}); - -export const bookmarkPermissionsSchema = z.object({ - [Permissions.USE]: z.boolean().default(true), -}); - -export const agentPermissionsSchema = z.object({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(false), - [Permissions.USE]: z.boolean().default(true), - [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(false), -}); - -export const multiConvoPermissionsSchema = z.object({ - [Permissions.USE]: z.boolean().default(true), -}); - -export const temporaryChatPermissionsSchema = z.object({ - [Permissions.USE]: z.boolean().default(true), -}); - -export const runCodePermissionsSchema = z.object({ - [Permissions.USE]: z.boolean().default(true), -}); - +// The role schema now only needs to reference the permissions schema. export const roleSchema = z.object({ name: z.string(), - [PermissionTypes.PROMPTS]: promptPermissionsSchema, - [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, - [PermissionTypes.AGENTS]: agentPermissionsSchema, - [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, - [PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema, - [PermissionTypes.RUN_CODE]: runCodePermissionsSchema, + permissions: permissionsSchema, }); export type TRole = z.infer; -export type TAgentPermissions = z.infer; -export type TPromptPermissions = z.infer; -export type TBookmarkPermissions = z.infer; -export type TMultiConvoPermissions = z.infer; -export type TTemporaryChatPermissions = z.infer; -export type TRunCodePermissions = z.infer; +// Define default roles using the new structure. const defaultRolesSchema = z.object({ [SystemRoles.ADMIN]: roleSchema.extend({ name: z.literal(SystemRoles.ADMIN), - [PermissionTypes.PROMPTS]: promptPermissionsSchema.extend({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(true), - [Permissions.USE]: z.boolean().default(true), - [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(true), - }), - [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema.extend({ - [Permissions.USE]: z.boolean().default(true), - }), - [PermissionTypes.AGENTS]: agentPermissionsSchema.extend({ - [Permissions.SHARED_GLOBAL]: z.boolean().default(true), - [Permissions.USE]: z.boolean().default(true), - [Permissions.CREATE]: z.boolean().default(true), - // [Permissions.SHARE]: z.boolean().default(true), - }), - [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema.extend({ - [Permissions.USE]: z.boolean().default(true), - }), - [PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema.extend({ - [Permissions.USE]: z.boolean().default(true), - }), - [PermissionTypes.RUN_CODE]: runCodePermissionsSchema.extend({ - [Permissions.USE]: z.boolean().default(true), + permissions: permissionsSchema.extend({ + [PermissionTypes.PROMPTS]: promptPermissionsSchema.extend({ + [Permissions.SHARED_GLOBAL]: z.boolean().default(true), + [Permissions.USE]: z.boolean().default(true), + [Permissions.CREATE]: z.boolean().default(true), + // [Permissions.SHARE]: z.boolean().default(true), + }), + [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema.extend({ + [Permissions.USE]: z.boolean().default(true), + }), + [PermissionTypes.AGENTS]: agentPermissionsSchema.extend({ + [Permissions.SHARED_GLOBAL]: z.boolean().default(true), + [Permissions.USE]: z.boolean().default(true), + [Permissions.CREATE]: z.boolean().default(true), + // [Permissions.SHARE]: z.boolean().default(true), + }), + [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema.extend({ + [Permissions.USE]: z.boolean().default(true), + }), + [PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema.extend({ + [Permissions.USE]: z.boolean().default(true), + }), + [PermissionTypes.RUN_CODE]: runCodePermissionsSchema.extend({ + [Permissions.USE]: z.boolean().default(true), + }), }), }), [SystemRoles.USER]: roleSchema.extend({ name: z.literal(SystemRoles.USER), - [PermissionTypes.PROMPTS]: promptPermissionsSchema, - [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, - [PermissionTypes.AGENTS]: agentPermissionsSchema, - [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, - [PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema, - [PermissionTypes.RUN_CODE]: runCodePermissionsSchema, + permissions: permissionsSchema, }), }); export const roleDefaults = defaultRolesSchema.parse({ [SystemRoles.ADMIN]: { name: SystemRoles.ADMIN, - [PermissionTypes.PROMPTS]: {}, - [PermissionTypes.BOOKMARKS]: {}, - [PermissionTypes.AGENTS]: {}, - [PermissionTypes.MULTI_CONVO]: {}, - [PermissionTypes.TEMPORARY_CHAT]: {}, - [PermissionTypes.RUN_CODE]: {}, + permissions: { + [PermissionTypes.PROMPTS]: {}, + [PermissionTypes.BOOKMARKS]: {}, + [PermissionTypes.AGENTS]: {}, + [PermissionTypes.MULTI_CONVO]: {}, + [PermissionTypes.TEMPORARY_CHAT]: {}, + [PermissionTypes.RUN_CODE]: {}, + }, }, [SystemRoles.USER]: { name: SystemRoles.USER, - [PermissionTypes.PROMPTS]: {}, - [PermissionTypes.BOOKMARKS]: {}, - [PermissionTypes.AGENTS]: {}, - [PermissionTypes.MULTI_CONVO]: {}, - [PermissionTypes.TEMPORARY_CHAT]: {}, - [PermissionTypes.RUN_CODE]: {}, + permissions: { + [PermissionTypes.PROMPTS]: {}, + [PermissionTypes.BOOKMARKS]: {}, + [PermissionTypes.AGENTS]: {}, + [PermissionTypes.MULTI_CONVO]: {}, + [PermissionTypes.TEMPORARY_CHAT]: {}, + [PermissionTypes.RUN_CODE]: {}, + }, }, }); diff --git a/packages/data-provider/src/types/mutations.ts b/packages/data-provider/src/types/mutations.ts index de04663b9..e2f358cf7 100644 --- a/packages/data-provider/src/types/mutations.ts +++ b/packages/data-provider/src/types/mutations.ts @@ -1,5 +1,6 @@ import * as types from '../types'; import * as r from '../roles'; +import * as p from '../permissions'; import { Tools, Assistant, @@ -251,9 +252,9 @@ export type UpdatePermVars = { updates: Partial; }; -export type UpdatePromptPermVars = UpdatePermVars; +export type UpdatePromptPermVars = UpdatePermVars; -export type UpdateAgentPermVars = UpdatePermVars; +export type UpdateAgentPermVars = UpdatePermVars; export type UpdatePermResponse = r.TRole; diff --git a/packages/data-schemas/package.json b/packages/data-schemas/package.json index 3add216e4..51f77aa9f 100644 --- a/packages/data-schemas/package.json +++ b/packages/data-schemas/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/data-schemas", - "version": "0.0.5", + "version": "0.0.6", "description": "Mongoose schemas and models for LibreChat", "type": "module", "main": "dist/index.cjs", diff --git a/packages/data-schemas/src/schema/role.ts b/packages/data-schemas/src/schema/role.ts index 9d3dd4c68..dd19634d2 100644 --- a/packages/data-schemas/src/schema/role.ts +++ b/packages/data-schemas/src/schema/role.ts @@ -3,88 +3,81 @@ import { PermissionTypes, Permissions } from 'librechat-data-provider'; export interface IRole extends Document { name: string; - [PermissionTypes.BOOKMARKS]?: { - [Permissions.USE]?: boolean; - }; - [PermissionTypes.PROMPTS]?: { - [Permissions.SHARED_GLOBAL]?: boolean; - [Permissions.USE]?: boolean; - [Permissions.CREATE]?: boolean; - }; - [PermissionTypes.AGENTS]?: { - [Permissions.SHARED_GLOBAL]?: boolean; - [Permissions.USE]?: boolean; - [Permissions.CREATE]?: boolean; - }; - [PermissionTypes.MULTI_CONVO]?: { - [Permissions.USE]?: boolean; - }; - [PermissionTypes.TEMPORARY_CHAT]?: { - [Permissions.USE]?: boolean; - }; - [PermissionTypes.RUN_CODE]?: { - [Permissions.USE]?: boolean; + permissions: { + [PermissionTypes.BOOKMARKS]?: { + [Permissions.USE]?: boolean; + }; + [PermissionTypes.PROMPTS]?: { + [Permissions.SHARED_GLOBAL]?: boolean; + [Permissions.USE]?: boolean; + [Permissions.CREATE]?: boolean; + }; + [PermissionTypes.AGENTS]?: { + [Permissions.SHARED_GLOBAL]?: boolean; + [Permissions.USE]?: boolean; + [Permissions.CREATE]?: boolean; + }; + [PermissionTypes.MULTI_CONVO]?: { + [Permissions.USE]?: boolean; + }; + [PermissionTypes.TEMPORARY_CHAT]?: { + [Permissions.USE]?: boolean; + }; + [PermissionTypes.RUN_CODE]?: { + [Permissions.USE]?: boolean; + }; }; } +// Create a sub-schema for permissions. Notice we disable _id for this subdocument. +const rolePermissionsSchema = new Schema( + { + [PermissionTypes.BOOKMARKS]: { + [Permissions.USE]: { type: Boolean, default: true }, + }, + [PermissionTypes.PROMPTS]: { + [Permissions.SHARED_GLOBAL]: { type: Boolean, default: false }, + [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.CREATE]: { type: Boolean, default: true }, + }, + [PermissionTypes.AGENTS]: { + [Permissions.SHARED_GLOBAL]: { type: Boolean, default: false }, + [Permissions.USE]: { type: Boolean, default: true }, + [Permissions.CREATE]: { type: Boolean, default: true }, + }, + [PermissionTypes.MULTI_CONVO]: { + [Permissions.USE]: { type: Boolean, default: true }, + }, + [PermissionTypes.TEMPORARY_CHAT]: { + [Permissions.USE]: { type: Boolean, default: true }, + }, + [PermissionTypes.RUN_CODE]: { + [Permissions.USE]: { type: Boolean, default: true }, + }, + }, + { _id: false }, +); + const roleSchema: Schema = new Schema({ - name: { - type: String, - required: true, - unique: true, - index: true, - }, - [PermissionTypes.BOOKMARKS]: { - [Permissions.USE]: { - type: Boolean, - default: true, - }, - }, - [PermissionTypes.PROMPTS]: { - [Permissions.SHARED_GLOBAL]: { - type: Boolean, - default: false, - }, - [Permissions.USE]: { - type: Boolean, - default: true, - }, - [Permissions.CREATE]: { - type: Boolean, - default: true, - }, - }, - [PermissionTypes.AGENTS]: { - [Permissions.SHARED_GLOBAL]: { - type: Boolean, - default: false, - }, - [Permissions.USE]: { - type: Boolean, - default: true, - }, - [Permissions.CREATE]: { - type: Boolean, - default: true, - }, - }, - [PermissionTypes.MULTI_CONVO]: { - [Permissions.USE]: { - type: Boolean, - default: true, - }, - }, - [PermissionTypes.TEMPORARY_CHAT]: { - [Permissions.USE]: { - type: Boolean, - default: true, - }, - }, - [PermissionTypes.RUN_CODE]: { - [Permissions.USE]: { - type: Boolean, - default: true, - }, + name: { type: String, required: true, unique: true, index: true }, + permissions: { + type: rolePermissionsSchema, + default: () => ({ + [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true }, + [PermissionTypes.PROMPTS]: { + [Permissions.SHARED_GLOBAL]: false, + [Permissions.USE]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.AGENTS]: { + [Permissions.SHARED_GLOBAL]: false, + [Permissions.USE]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true }, + [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true }, + [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true }, + }), }, });