Skip to content

Commit

Permalink
🔐 feat: Toggle Access to Prompts via librechat.yaml (#3735)
Browse files Browse the repository at this point in the history
* chore: update CONFIG_VERSION to '1.1.6'

* chore: update package version to 0.7.415

* feat: toggle USER role access to prompts via librechat.yaml

* refactor: set prompts to true when loadDefaultInterface returns true

* ci(AppService): mock updatePromptsAccess
  • Loading branch information
danny-avila authored Aug 21, 2024
1 parent 0c5568b commit 596ecc6
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 11 deletions.
42 changes: 41 additions & 1 deletion api/models/Role.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
const { SystemRoles, CacheKeys, roleDefaults } = require('librechat-data-provider');
const {
SystemRoles,
CacheKeys,
roleDefaults,
PermissionTypes,
Permissions,
promptPermissionsSchema,
} = require('librechat-data-provider');
const getLogStores = require('~/cache/getLogStores');
const Role = require('~/models/schema/roleSchema');
const { logger } = require('~/config');

/**
* Retrieve a role by name and convert the found role document to a plain object.
Expand Down Expand Up @@ -61,6 +69,37 @@ const updateRoleByName = async function (roleName, updates) {
}
};

/**
* Updates the Prompt access for a specific role.
* @param {SystemRoles} roleName - The role to update the prompt access for.
* @param {boolean | undefined} [value] - The new value for the prompt access.
*/
async function updatePromptsAccess(roleName, value) {
if (typeof value === 'undefined') {
return;
}

try {
const parsedUpdates = promptPermissionsSchema.partial().parse({ [Permissions.USE]: value });
const role = await getRoleByName(roleName);
if (!role) {
return;
}

const mergedUpdates = {
[PermissionTypes.PROMPTS]: {
...role[PermissionTypes.PROMPTS],
...parsedUpdates,
},
};

await updateRoleByName(roleName, mergedUpdates);
logger.info(`Updated '${roleName}' role prompts 'USE' permission to: ${value}`);
} catch (error) {
logger.error('Failed to update USER role prompts USE permission:', error);
}
}

/**
* Initialize default roles in the system.
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
Expand All @@ -83,4 +122,5 @@ module.exports = {
getRoleByName,
initializeRoles,
updateRoleByName,
updatePromptsAccess,
};
73 changes: 73 additions & 0 deletions api/server/services/AppService.interface.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
jest.mock('~/models/Role', () => ({
initializeRoles: jest.fn(),
updatePromptsAccess: jest.fn(),
getRoleByName: jest.fn(),
updateRoleByName: jest.fn(),
}));

jest.mock('~/config', () => ({
logger: {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
},
}));

jest.mock('./Config/loadCustomConfig', () => jest.fn());
jest.mock('./start/interface', () => ({
loadDefaultInterface: jest.fn(),
}));
jest.mock('./ToolService', () => ({
loadAndFormatTools: jest.fn().mockReturnValue({}),
}));
jest.mock('./start/checks', () => ({
checkVariables: jest.fn(),
checkHealth: jest.fn(),
checkConfig: jest.fn(),
checkAzureVariables: jest.fn(),
}));

const AppService = require('./AppService');
const { loadDefaultInterface } = require('./start/interface');

describe('AppService interface.prompts configuration', () => {
let app;
let mockLoadCustomConfig;

beforeEach(() => {
app = { locals: {} };
jest.resetModules();
jest.clearAllMocks();
mockLoadCustomConfig = require('./Config/loadCustomConfig');
});

it('should set prompts to true when loadDefaultInterface returns true', async () => {
mockLoadCustomConfig.mockResolvedValue({});
loadDefaultInterface.mockResolvedValue({ prompts: true });

await AppService(app);

expect(app.locals.interfaceConfig.prompts).toBe(true);
expect(loadDefaultInterface).toHaveBeenCalled();
});

it('should set prompts to false when loadDefaultInterface returns false', async () => {
mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: false } });
loadDefaultInterface.mockResolvedValue({ prompts: false });

await AppService(app);

expect(app.locals.interfaceConfig.prompts).toBe(false);
expect(loadDefaultInterface).toHaveBeenCalled();
});

it('should not set prompts when loadDefaultInterface returns undefined', async () => {
mockLoadCustomConfig.mockResolvedValue({});
loadDefaultInterface.mockResolvedValue({});

await AppService(app);

expect(app.locals.interfaceConfig.prompts).toBeUndefined();
expect(loadDefaultInterface).toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion api/server/services/AppService.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const AppService = async (app) => {

const socialLogins =
config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins;
const interfaceConfig = loadDefaultInterface(config, configDefaults);
const interfaceConfig = await loadDefaultInterface(config, configDefaults);

const defaultLocals = {
paths,
Expand Down
3 changes: 1 addition & 2 deletions api/server/services/AppService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jest.mock('./Files/Firebase/initialize', () => ({
}));
jest.mock('~/models/Role', () => ({
initializeRoles: jest.fn(),
updatePromptsAccess: jest.fn(),
}));
jest.mock('./ToolService', () => ({
loadAndFormatTools: jest.fn().mockReturnValue({
Expand Down Expand Up @@ -97,8 +98,6 @@ describe('AppService', () => {
socialLogins: ['testLogin'],
fileStrategy: 'testStrategy',
interfaceConfig: expect.objectContaining({
privacyPolicy: undefined,
termsOfService: undefined,
endpointsMenu: true,
modelSelect: true,
parameters: true,
Expand Down
15 changes: 11 additions & 4 deletions api/server/services/start/interface.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
const { SystemRoles, removeNullishValues } = require('librechat-data-provider');
const { updatePromptsAccess } = require('~/models/Role');
const { logger } = require('~/config');

/**
* Loads the default interface object.
* @param {TCustomConfig | undefined} config - The loaded custom configuration.
* @param {TConfigDefaults} configDefaults - The custom configuration default values.
* @returns {TCustomConfig['interface']} The default interface object.
* @param {SystemRoles} [roleName] - The role to load the default interface for, defaults to `'USER'`.
* @returns {Promise<TCustomConfig['interface']>} The default interface object.
*/
function loadDefaultInterface(config, configDefaults) {
async function loadDefaultInterface(config, configDefaults, roleName = SystemRoles.USER) {
const { interface: interfaceConfig } = config ?? {};
const { interface: defaults } = configDefaults;
const hasModelSpecs = config?.modelSpecs?.list?.length > 0;

const loadedInterface = {
/** @type {TCustomConfig['interface']} */
const loadedInterface = removeNullishValues({
endpointsMenu:
interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu),
modelSelect: interfaceConfig?.modelSelect ?? (hasModelSpecs ? false : defaults.modelSelect),
Expand All @@ -20,7 +24,10 @@ function loadDefaultInterface(config, configDefaults) {
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
};
prompts: interfaceConfig?.prompts ?? defaults.prompts,
});

await updatePromptsAccess(roleName, loadedInterface.prompts);

let i = 0;
const logSettings = () => {
Expand Down
45 changes: 45 additions & 0 deletions api/server/services/start/interface.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const { SystemRoles } = require('librechat-data-provider');
const { updatePromptsAccess } = require('~/models/Role');
const { loadDefaultInterface } = require('./interface');

jest.mock('~/models/Role', () => ({
updatePromptsAccess: jest.fn(),
}));

describe('loadDefaultInterface', () => {
it('should call updatePromptsAccess with the correct parameters when prompts is true', async () => {
const config = { interface: { prompts: true } };
const configDefaults = { interface: {} };

await loadDefaultInterface(config, configDefaults);

expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, true);
});

it('should call updatePromptsAccess with false when prompts is false', async () => {
const config = { interface: { prompts: false } };
const configDefaults = { interface: {} };

await loadDefaultInterface(config, configDefaults);

expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, false);
});

it('should call updatePromptsAccess with undefined when prompts is not specified in config', async () => {
const config = {};
const configDefaults = { interface: {} };

await loadDefaultInterface(config, configDefaults);

expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined);
});

it('should call updatePromptsAccess with undefined when prompts is explicitly undefined', async () => {
const config = { interface: { prompts: undefined } };
const configDefaults = { interface: {} };

await loadDefaultInterface(config, configDefaults);

expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined);
});
});
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/data-provider/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.7.414",
"version": "0.7.415",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",
Expand Down
3 changes: 2 additions & 1 deletion packages/data-provider/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ export const configSchema = z.object({
parameters: z.boolean().optional(),
sidePanel: z.boolean().optional(),
presets: z.boolean().optional(),
prompts: z.boolean().optional(),
})
.default({
endpointsMenu: true,
Expand Down Expand Up @@ -944,7 +945,7 @@ export enum Constants {
/** Key for the app's version. */
VERSION = 'v0.7.4',
/** Key for the Custom Config's version (librechat.yaml). */
CONFIG_VERSION = '1.1.5',
CONFIG_VERSION = '1.1.6',
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
NO_PARENT = '00000000-0000-0000-0000-000000000000',
/** Standard value for the initial conversationId before a request is sent */
Expand Down

0 comments on commit 596ecc6

Please sign in to comment.