mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🎏 refactor: Streamline Role Permissions from Interface Config
This commit is contained in:
parent
b742c8c7f9
commit
e8ddd279fd
5 changed files with 1008 additions and 231 deletions
|
|
@ -995,23 +995,4 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
|||
roles: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default peoplePicker permissions when not specified', async () => {
|
||||
const mockConfig = {
|
||||
interface: {
|
||||
// No peoplePicker configuration
|
||||
},
|
||||
};
|
||||
|
||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||
|
||||
const app = { locals: {} };
|
||||
await AppService(app);
|
||||
|
||||
// Check that default permissions are applied
|
||||
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
|
||||
expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true);
|
||||
expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(true);
|
||||
expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
const {
|
||||
SystemRoles,
|
||||
Permissions,
|
||||
roleDefaults,
|
||||
PermissionTypes,
|
||||
removeNullishValues,
|
||||
} = require('librechat-data-provider');
|
||||
|
|
@ -8,43 +9,6 @@ const { logger } = require('@librechat/data-schemas');
|
|||
const { isMemoryEnabled } = require('@librechat/api');
|
||||
const { updateAccessPermissions, getRoleByName } = require('~/models/Role');
|
||||
|
||||
/**
|
||||
* Updates role permissions intelligently - only updates permission types that:
|
||||
* 1. Don't exist in the database (first time setup)
|
||||
* 2. Are explicitly configured in the config file
|
||||
* @param {object} params - The role name to update
|
||||
* @param {string} params.roleName - The role name to update
|
||||
* @param {object} params.allPermissions - All permissions to potentially update
|
||||
* @param {object} params.interfaceConfig - The interface config from librechat.yaml
|
||||
*/
|
||||
async function updateRolePermissions({ roleName, allPermissions, interfaceConfig }) {
|
||||
const existingRole = await getRoleByName(roleName);
|
||||
const existingPermissions = existingRole?.permissions || {};
|
||||
const permissionsToUpdate = {};
|
||||
|
||||
for (const [permType, perms] of Object.entries(allPermissions)) {
|
||||
const permTypeExists = existingPermissions[permType];
|
||||
|
||||
const isExplicitlyConfigured = interfaceConfig && hasExplicitConfig(interfaceConfig, permType);
|
||||
|
||||
// Only update if: doesn't exist OR explicitly configured
|
||||
if (!permTypeExists || isExplicitlyConfigured) {
|
||||
permissionsToUpdate[permType] = perms;
|
||||
if (!permTypeExists) {
|
||||
logger.debug(`Role '${roleName}': Setting up default permissions for '${permType}'`);
|
||||
} else if (isExplicitlyConfigured) {
|
||||
logger.debug(`Role '${roleName}': Applying explicit config for '${permType}'`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`Role '${roleName}': Preserving existing permissions for '${permType}'`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(permissionsToUpdate).length > 0) {
|
||||
await updateAccessPermissions(roleName, permissionsToUpdate, existingRole);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a permission type has explicit configuration
|
||||
*/
|
||||
|
|
@ -101,6 +65,7 @@ async function loadDefaultInterface(config, configDefaults) {
|
|||
|
||||
/** @type {TCustomConfig['interface']} */
|
||||
const loadedInterface = removeNullishValues({
|
||||
// UI elements - use schema defaults
|
||||
endpointsMenu:
|
||||
interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu),
|
||||
modelSelect:
|
||||
|
|
@ -112,55 +77,176 @@ async function loadDefaultInterface(config, configDefaults) {
|
|||
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
|
||||
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
|
||||
mcpServers: interfaceConfig?.mcpServers ?? defaults.mcpServers,
|
||||
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
|
||||
memories: shouldDisableMemories ? false : (interfaceConfig?.memories ?? defaults.memories),
|
||||
prompts: interfaceConfig?.prompts ?? defaults.prompts,
|
||||
multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo,
|
||||
agents: interfaceConfig?.agents ?? defaults.agents,
|
||||
temporaryChat: interfaceConfig?.temporaryChat ?? defaults.temporaryChat,
|
||||
runCode: interfaceConfig?.runCode ?? defaults.runCode,
|
||||
webSearch: interfaceConfig?.webSearch ?? defaults.webSearch,
|
||||
fileSearch: interfaceConfig?.fileSearch ?? defaults.fileSearch,
|
||||
fileCitations: interfaceConfig?.fileCitations ?? defaults.fileCitations,
|
||||
customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome,
|
||||
peoplePicker: {
|
||||
users: interfaceConfig?.peoplePicker?.users ?? defaults.peoplePicker?.users,
|
||||
groups: interfaceConfig?.peoplePicker?.groups ?? defaults.peoplePicker?.groups,
|
||||
roles: interfaceConfig?.peoplePicker?.roles ?? defaults.peoplePicker?.roles,
|
||||
},
|
||||
marketplace: {
|
||||
use: interfaceConfig?.marketplace?.use ?? defaults.marketplace?.use,
|
||||
},
|
||||
|
||||
// Permissions - only include if explicitly configured
|
||||
bookmarks: interfaceConfig?.bookmarks,
|
||||
memories: shouldDisableMemories ? false : interfaceConfig?.memories,
|
||||
prompts: interfaceConfig?.prompts,
|
||||
multiConvo: interfaceConfig?.multiConvo,
|
||||
agents: interfaceConfig?.agents,
|
||||
temporaryChat: interfaceConfig?.temporaryChat,
|
||||
runCode: interfaceConfig?.runCode,
|
||||
webSearch: interfaceConfig?.webSearch,
|
||||
fileSearch: interfaceConfig?.fileSearch,
|
||||
fileCitations: interfaceConfig?.fileCitations,
|
||||
peoplePicker: interfaceConfig?.peoplePicker,
|
||||
marketplace: interfaceConfig?.marketplace,
|
||||
});
|
||||
|
||||
// Helper to get permission value with proper precedence
|
||||
const getPermissionValue = (configValue, roleDefault, schemaDefault) => {
|
||||
if (configValue !== undefined) return configValue;
|
||||
if (roleDefault !== undefined) return roleDefault;
|
||||
return schemaDefault;
|
||||
};
|
||||
|
||||
// Permission precedence order:
|
||||
// 1. Explicit user configuration (from librechat.yaml)
|
||||
// 2. Role-specific defaults (from roleDefaults)
|
||||
// 3. Interface schema defaults (from interfaceSchema.default())
|
||||
for (const roleName of [SystemRoles.USER, SystemRoles.ADMIN]) {
|
||||
await updateRolePermissions({
|
||||
roleName,
|
||||
allPermissions: {
|
||||
[PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts },
|
||||
[PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks },
|
||||
const defaultPerms = roleDefaults[roleName].permissions;
|
||||
const existingRole = await getRoleByName(roleName);
|
||||
const existingPermissions = existingRole?.permissions || {};
|
||||
const permissionsToUpdate = {};
|
||||
|
||||
// Helper to add permission if it should be updated
|
||||
const addPermissionIfNeeded = (permType, permissions) => {
|
||||
const permTypeExists = existingPermissions[permType];
|
||||
const isExplicitlyConfigured =
|
||||
interfaceConfig && hasExplicitConfig(interfaceConfig, permType);
|
||||
|
||||
// Only update if: doesn't exist OR explicitly configured
|
||||
if (!permTypeExists || isExplicitlyConfigured) {
|
||||
permissionsToUpdate[permType] = permissions;
|
||||
if (!permTypeExists) {
|
||||
logger.debug(`Role '${roleName}': Setting up default permissions for '${permType}'`);
|
||||
} else if (isExplicitlyConfigured) {
|
||||
logger.debug(`Role '${roleName}': Applying explicit config for '${permType}'`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(`Role '${roleName}': Preserving existing permissions for '${permType}'`);
|
||||
}
|
||||
};
|
||||
|
||||
// Build permissions for each type
|
||||
const allPermissions = {
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.prompts,
|
||||
defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.USE],
|
||||
defaults.prompts,
|
||||
),
|
||||
[Permissions.SHARED_GLOBAL]:
|
||||
defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.SHARED_GLOBAL],
|
||||
[Permissions.CREATE]: defaultPerms[PermissionTypes.PROMPTS]?.[Permissions.CREATE],
|
||||
},
|
||||
[PermissionTypes.BOOKMARKS]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.bookmarks,
|
||||
defaultPerms[PermissionTypes.BOOKMARKS]?.[Permissions.USE],
|
||||
defaults.bookmarks,
|
||||
),
|
||||
},
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: loadedInterface.memories,
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.memories,
|
||||
defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.USE],
|
||||
defaults.memories,
|
||||
),
|
||||
[Permissions.CREATE]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.CREATE],
|
||||
[Permissions.UPDATE]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.UPDATE],
|
||||
[Permissions.READ]: defaultPerms[PermissionTypes.MEMORIES]?.[Permissions.READ],
|
||||
[Permissions.OPT_OUT]: isPersonalizationEnabled,
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo },
|
||||
[PermissionTypes.AGENTS]: { [Permissions.USE]: loadedInterface.agents },
|
||||
[PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: loadedInterface.temporaryChat },
|
||||
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode },
|
||||
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch },
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.multiConvo,
|
||||
defaultPerms[PermissionTypes.MULTI_CONVO]?.[Permissions.USE],
|
||||
defaults.multiConvo,
|
||||
),
|
||||
},
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.agents,
|
||||
defaultPerms[PermissionTypes.AGENTS]?.[Permissions.USE],
|
||||
defaults.agents,
|
||||
),
|
||||
[Permissions.SHARED_GLOBAL]:
|
||||
defaultPerms[PermissionTypes.AGENTS]?.[Permissions.SHARED_GLOBAL],
|
||||
[Permissions.CREATE]: defaultPerms[PermissionTypes.AGENTS]?.[Permissions.CREATE],
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.temporaryChat,
|
||||
defaultPerms[PermissionTypes.TEMPORARY_CHAT]?.[Permissions.USE],
|
||||
defaults.temporaryChat,
|
||||
),
|
||||
},
|
||||
[PermissionTypes.RUN_CODE]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.runCode,
|
||||
defaultPerms[PermissionTypes.RUN_CODE]?.[Permissions.USE],
|
||||
defaults.runCode,
|
||||
),
|
||||
},
|
||||
[PermissionTypes.WEB_SEARCH]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.webSearch,
|
||||
defaultPerms[PermissionTypes.WEB_SEARCH]?.[Permissions.USE],
|
||||
defaults.webSearch,
|
||||
),
|
||||
},
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: loadedInterface.peoplePicker?.users,
|
||||
[Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker?.groups,
|
||||
[Permissions.VIEW_ROLES]: loadedInterface.peoplePicker?.roles,
|
||||
[Permissions.VIEW_USERS]: getPermissionValue(
|
||||
loadedInterface.peoplePicker?.users,
|
||||
defaultPerms[PermissionTypes.PEOPLE_PICKER]?.[Permissions.VIEW_USERS],
|
||||
defaults.peoplePicker?.users,
|
||||
),
|
||||
[Permissions.VIEW_GROUPS]: getPermissionValue(
|
||||
loadedInterface.peoplePicker?.groups,
|
||||
defaultPerms[PermissionTypes.PEOPLE_PICKER]?.[Permissions.VIEW_GROUPS],
|
||||
defaults.peoplePicker?.groups,
|
||||
),
|
||||
[Permissions.VIEW_ROLES]: getPermissionValue(
|
||||
loadedInterface.peoplePicker?.roles,
|
||||
defaultPerms[PermissionTypes.PEOPLE_PICKER]?.[Permissions.VIEW_ROLES],
|
||||
defaults.peoplePicker?.roles,
|
||||
),
|
||||
},
|
||||
[PermissionTypes.MARKETPLACE]: {
|
||||
[Permissions.USE]: loadedInterface.marketplace?.use,
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.marketplace?.use,
|
||||
defaultPerms[PermissionTypes.MARKETPLACE]?.[Permissions.USE],
|
||||
defaults.marketplace?.use,
|
||||
),
|
||||
},
|
||||
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch },
|
||||
[PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: loadedInterface.fileCitations },
|
||||
[PermissionTypes.FILE_SEARCH]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.fileSearch,
|
||||
defaultPerms[PermissionTypes.FILE_SEARCH]?.[Permissions.USE],
|
||||
defaults.fileSearch,
|
||||
),
|
||||
},
|
||||
interfaceConfig,
|
||||
});
|
||||
[PermissionTypes.FILE_CITATIONS]: {
|
||||
[Permissions.USE]: getPermissionValue(
|
||||
loadedInterface.fileCitations,
|
||||
defaultPerms[PermissionTypes.FILE_CITATIONS]?.[Permissions.USE],
|
||||
defaults.fileCitations,
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
// Check and add each permission type if needed
|
||||
for (const [permType, permissions] of Object.entries(allPermissions)) {
|
||||
addPermissionIfNeeded(permType, permissions);
|
||||
}
|
||||
|
||||
// Update permissions if any need updating
|
||||
if (Object.keys(permissionsToUpdate).length > 0) {
|
||||
await updateAccessPermissions(roleName, permissionsToUpdate, existingRole);
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -15,14 +15,12 @@ export function createRoleMethods(mongoose: typeof import('mongoose')) {
|
|||
const defaultPerms = roleDefaults[roleName].permissions;
|
||||
|
||||
if (!role) {
|
||||
// Create new role if it doesn't exist.
|
||||
role = new Role(roleDefaults[roleName]);
|
||||
} else {
|
||||
// Ensure role.permissions is defined.
|
||||
const permissions = role.toObject()?.permissions ?? {};
|
||||
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) {
|
||||
if (permissions[permType] == null || Object.keys(permissions[permType]).length === 0) {
|
||||
role.permissions[permType] = defaultPerms[permType as keyof typeof defaultPerms];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,50 +8,50 @@ import type { IRole } from '~/types';
|
|||
const rolePermissionsSchema = new Schema(
|
||||
{
|
||||
[PermissionTypes.BOOKMARKS]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.PROMPTS]: {
|
||||
[Permissions.SHARED_GLOBAL]: { type: Boolean, default: false },
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.CREATE]: { type: Boolean, default: true },
|
||||
[Permissions.SHARED_GLOBAL]: { type: Boolean },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
[Permissions.CREATE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.MEMORIES]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.CREATE]: { type: Boolean, default: true },
|
||||
[Permissions.UPDATE]: { type: Boolean, default: true },
|
||||
[Permissions.READ]: { type: Boolean, default: true },
|
||||
[Permissions.OPT_OUT]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
[Permissions.CREATE]: { type: Boolean },
|
||||
[Permissions.UPDATE]: { type: Boolean },
|
||||
[Permissions.READ]: { type: Boolean },
|
||||
[Permissions.OPT_OUT]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.AGENTS]: {
|
||||
[Permissions.SHARED_GLOBAL]: { type: Boolean, default: false },
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.CREATE]: { type: Boolean, default: true },
|
||||
[Permissions.SHARED_GLOBAL]: { type: Boolean },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
[Permissions.CREATE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.MULTI_CONVO]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.TEMPORARY_CHAT]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.RUN_CODE]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.WEB_SEARCH]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: { type: Boolean, default: false },
|
||||
[Permissions.VIEW_GROUPS]: { type: Boolean, default: false },
|
||||
[Permissions.VIEW_ROLES]: { type: Boolean, default: false },
|
||||
[Permissions.VIEW_USERS]: { type: Boolean },
|
||||
[Permissions.VIEW_GROUPS]: { type: Boolean },
|
||||
[Permissions.VIEW_ROLES]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.MARKETPLACE]: {
|
||||
[Permissions.USE]: { type: Boolean, default: false },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.FILE_SEARCH]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
[PermissionTypes.FILE_CITATIONS]: {
|
||||
[Permissions.USE]: { type: Boolean, default: true },
|
||||
[Permissions.USE]: { type: Boolean },
|
||||
},
|
||||
},
|
||||
{ _id: false },
|
||||
|
|
@ -61,37 +61,6 @@ const roleSchema: Schema<IRole> = new Schema({
|
|||
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.MEMORIES]: {
|
||||
[Permissions.USE]: true,
|
||||
[Permissions.CREATE]: true,
|
||||
[Permissions.UPDATE]: true,
|
||||
[Permissions.READ]: 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 },
|
||||
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: false,
|
||||
[Permissions.VIEW_GROUPS]: false,
|
||||
[Permissions.VIEW_ROLES]: false,
|
||||
},
|
||||
[PermissionTypes.MARKETPLACE]: { [Permissions.USE]: false },
|
||||
[PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
|
||||
[PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: true },
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue