mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-07 00:15:23 +02:00
* fix: Resolve custom role permissions not loading in frontend Users assigned to custom roles (non-USER/ADMIN) had all permission checks fail because AuthContext only fetched system role permissions. The roles map keyed by USER/ADMIN never contained the custom role name, so useHasAccess returned false for every feature gate. - Fetch the user's custom role in AuthContext and include it in the roles map so useHasAccess can resolve permissions correctly - Use encodeURIComponent instead of toLowerCase for role name URLs to preserve custom role casing through the API roundtrip - Only uppercase system role names on the backend GET route; pass custom role names through as-is for exact DB lookup - Allow users to fetch their own assigned role without READ_ROLES capability * refactor: Normalize all role names to uppercase Custom role names were stored in original casing, causing case-sensitivity bugs across the stack — URL lowercasing, route uppercasing, and case-sensitive DB lookups all conflicted for mixed-case custom roles. Enforce uppercase normalization at every boundary: - createRoleByName trims and uppercases the name before storage - createRoleHandler uppercases before passing to createRoleByName - All admin route handlers (get, update, delete, members, permissions) uppercase the :name URL param before DB lookups - addRoleMemberHandler uppercases before setting user.role - Startup migration (normalizeRoleNames) finds non-uppercase custom roles, renames them, and updates affected user.role values with collision detection Legacy GET /api/roles/:roleName retains always-uppercase behavior. Tests updated to expect uppercase role names throughout. * fix: Use case-preserved role names with strict equality Remove uppercase normalization — custom role names are stored and compared exactly as the user sets them, with only trimming applied. USER and ADMIN remain reserved case-insensitively via isSystemRoleName. - Remove toUpperCase from createRoleByName, createRoleHandler, and all admin route handlers (get, update, delete, members, permissions) - Remove toUpperCase from legacy GET and PUT routes in roles.js; the frontend now sends exact casing via encodeURIComponent - Remove normalizeRoleNames startup migration - Revert test expectations to original casing * fix: Format useMemo dependency array for Prettier * feat: Add custom role support to admin settings + review fixes - Add backend tests for isOwnRole authorization gate on GET /api/roles/:roleName - Add frontend tests for custom role detection and fetching in AuthContext - Fix transient null permission flash by only spreading custom role once loaded - Add isSystemRoleName helper to data-provider for case-insensitive system role detection - Use sentinel value in useGetRole to avoid ghost cache entry from empty string - Add useListRoles hook and listRoles data service for fetching all roles - Update AdminSettingsDialog and PeoplePickerAdminSettings to dynamically list custom roles in the role dropdown, with proper fallback defaults * fix: Address review findings for custom role permissions - Add assertions to AuthContext test verifying custom role in roles map - Fix empty array bypassing nullish coalescing fallback in role dropdowns - Add null/undefined guard to isSystemRoleName helper - Memoize role dropdown items to avoid unnecessary re-renders - Apply sentinel pattern to useGetRole in admin settings for consistency - Mark ListRolesResponse description as required to match schema * fix: Prevent prototype pollution in role authorization gate - Replace roleDefaults[roleName] with Object.hasOwn to prevent prototype chain bypass for names like constructor or __proto__ - Add dedicated rolesList query key to avoid cache collision when a custom role is named 'list' - Add regression test for prototype property name authorization * fix: Resolve Prettier formatting and unused variable lint errors * fix: Address review findings for custom role permissions - Add ADMIN self-read test documenting isOwnRole bypass behavior - Guard save button while custom role data loads to prevent data loss - Extract useRoleSelector hook eliminating ~55 lines of duplication - Unify defaultValues/useEffect permission resolution (fixes inconsistency) - Make ListRolesResponse.description and _id optional to match schema - Fix vacuous test assertions to verify sentinel calls exist - Only fetch userRole when user.role === USER (avoid unnecessary requests) - Remove redundant empty string guard in custom role detection * fix: Revert USER role fetch restriction to preserve admin settings Admins need the USER role loaded in AuthContext.roles so the admin settings dialog shows persisted USER permissions instead of defaults. * fix: Remove unused useEffect import from useRoleSelector * fix: Clean up useRoleSelector hook - Use existing isCustom variable instead of re-calling isSystemRoleName - Remove unused roles and availableRoleNames from return object * fix: Address review findings for custom role permissions - Use Set-based isSystemRoleName to auto-expand with future SystemRoles - Add isCustomRoleError handling: guard useEffect reset and disable Save - Remove resolvePermissions from hook return; use defaultValues in useEffect to eliminate redundant computation and stale-closure reset race - Rename customRoleName to userRoleName in AuthContext for clarity * fix: Request server-max roles for admin dropdown listRoles now passes limit=200 (the server's MAX_PAGE_LIMIT) so the admin role selector shows all roles instead of silently truncating at the default page size of 50. --------- Co-authored-by: Danny Avila <danny@librechat.ai>
235 lines
7.6 KiB
TypeScript
235 lines
7.6 KiB
TypeScript
import { z } from 'zod';
|
|
import {
|
|
Permissions,
|
|
PermissionTypes,
|
|
permissionsSchema,
|
|
agentPermissionsSchema,
|
|
promptPermissionsSchema,
|
|
memoryPermissionsSchema,
|
|
runCodePermissionsSchema,
|
|
bookmarkPermissionsSchema,
|
|
webSearchPermissionsSchema,
|
|
fileSearchPermissionsSchema,
|
|
multiConvoPermissionsSchema,
|
|
mcpServersPermissionsSchema,
|
|
peoplePickerPermissionsSchema,
|
|
remoteAgentsPermissionsSchema,
|
|
temporaryChatPermissionsSchema,
|
|
fileCitationsPermissionsSchema,
|
|
} from './permissions';
|
|
|
|
/**
|
|
* Enum for System Defined Roles
|
|
*/
|
|
export enum SystemRoles {
|
|
/**
|
|
* The Admin role
|
|
*/
|
|
ADMIN = 'ADMIN',
|
|
/**
|
|
* The default user role
|
|
*/
|
|
USER = 'USER',
|
|
}
|
|
|
|
export const roleSchema = z.object({
|
|
name: z.string(),
|
|
permissions: permissionsSchema,
|
|
});
|
|
|
|
export type TRole = z.infer<typeof roleSchema>;
|
|
|
|
const defaultRolesSchema = z.object({
|
|
[SystemRoles.ADMIN]: roleSchema.extend({
|
|
name: z.literal(SystemRoles.ADMIN),
|
|
permissions: permissionsSchema.extend({
|
|
[PermissionTypes.PROMPTS]: promptPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
[Permissions.CREATE]: z.boolean().default(true),
|
|
[Permissions.SHARE]: z.boolean().default(true),
|
|
[Permissions.SHARE_PUBLIC]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.MEMORIES]: memoryPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
[Permissions.CREATE]: z.boolean().default(true),
|
|
[Permissions.UPDATE]: z.boolean().default(true),
|
|
[Permissions.READ]: z.boolean().default(true),
|
|
[Permissions.OPT_OUT]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.AGENTS]: agentPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
[Permissions.CREATE]: z.boolean().default(true),
|
|
[Permissions.SHARE]: z.boolean().default(true),
|
|
[Permissions.SHARE_PUBLIC]: 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),
|
|
}),
|
|
[PermissionTypes.WEB_SEARCH]: webSearchPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.PEOPLE_PICKER]: peoplePickerPermissionsSchema.extend({
|
|
[Permissions.VIEW_USERS]: z.boolean().default(true),
|
|
[Permissions.VIEW_GROUPS]: z.boolean().default(true),
|
|
[Permissions.VIEW_ROLES]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.MARKETPLACE]: z.object({
|
|
[Permissions.USE]: z.boolean().default(false),
|
|
}),
|
|
[PermissionTypes.FILE_SEARCH]: fileSearchPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.FILE_CITATIONS]: fileCitationsPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.MCP_SERVERS]: mcpServersPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
[Permissions.CREATE]: z.boolean().default(true),
|
|
[Permissions.SHARE]: z.boolean().default(true),
|
|
[Permissions.SHARE_PUBLIC]: z.boolean().default(true),
|
|
}),
|
|
[PermissionTypes.REMOTE_AGENTS]: remoteAgentsPermissionsSchema.extend({
|
|
[Permissions.USE]: z.boolean().default(true),
|
|
[Permissions.CREATE]: z.boolean().default(true),
|
|
[Permissions.SHARE]: z.boolean().default(true),
|
|
[Permissions.SHARE_PUBLIC]: z.boolean().default(true),
|
|
}),
|
|
}),
|
|
}),
|
|
[SystemRoles.USER]: roleSchema.extend({
|
|
name: z.literal(SystemRoles.USER),
|
|
permissions: permissionsSchema,
|
|
}),
|
|
});
|
|
|
|
const systemRoleSet = new Set(Object.values(SystemRoles).map((r) => r.toUpperCase()));
|
|
|
|
/** Case-insensitive check for reserved system role names. */
|
|
export function isSystemRoleName(name: string | undefined | null): boolean {
|
|
if (!name) {
|
|
return false;
|
|
}
|
|
return systemRoleSet.has(name.toUpperCase());
|
|
}
|
|
|
|
export const roleDefaults = defaultRolesSchema.parse({
|
|
[SystemRoles.ADMIN]: {
|
|
name: SystemRoles.ADMIN,
|
|
permissions: {
|
|
[PermissionTypes.PROMPTS]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: true,
|
|
[Permissions.SHARE]: true,
|
|
[Permissions.SHARE_PUBLIC]: true,
|
|
},
|
|
[PermissionTypes.BOOKMARKS]: {
|
|
[Permissions.USE]: true,
|
|
},
|
|
[PermissionTypes.MEMORIES]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: true,
|
|
[Permissions.UPDATE]: true,
|
|
[Permissions.READ]: true,
|
|
[Permissions.OPT_OUT]: true,
|
|
},
|
|
[PermissionTypes.AGENTS]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: true,
|
|
[Permissions.SHARE]: true,
|
|
[Permissions.SHARE_PUBLIC]: 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]: true,
|
|
[Permissions.VIEW_GROUPS]: true,
|
|
[Permissions.VIEW_ROLES]: true,
|
|
},
|
|
[PermissionTypes.MARKETPLACE]: {
|
|
[Permissions.USE]: true,
|
|
},
|
|
[PermissionTypes.FILE_SEARCH]: {
|
|
[Permissions.USE]: true,
|
|
},
|
|
[PermissionTypes.FILE_CITATIONS]: {
|
|
[Permissions.USE]: true,
|
|
},
|
|
[PermissionTypes.MCP_SERVERS]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: true,
|
|
[Permissions.SHARE]: true,
|
|
[Permissions.SHARE_PUBLIC]: true,
|
|
},
|
|
[PermissionTypes.REMOTE_AGENTS]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: true,
|
|
[Permissions.SHARE]: true,
|
|
[Permissions.SHARE_PUBLIC]: true,
|
|
},
|
|
},
|
|
},
|
|
[SystemRoles.USER]: {
|
|
name: SystemRoles.USER,
|
|
permissions: {
|
|
[PermissionTypes.PROMPTS]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: true,
|
|
[Permissions.SHARE]: false,
|
|
[Permissions.SHARE_PUBLIC]: false,
|
|
},
|
|
[PermissionTypes.BOOKMARKS]: {},
|
|
[PermissionTypes.MEMORIES]: {},
|
|
[PermissionTypes.AGENTS]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: true,
|
|
[Permissions.SHARE]: false,
|
|
[Permissions.SHARE_PUBLIC]: false,
|
|
},
|
|
[PermissionTypes.MULTI_CONVO]: {},
|
|
[PermissionTypes.TEMPORARY_CHAT]: {},
|
|
[PermissionTypes.RUN_CODE]: {},
|
|
[PermissionTypes.WEB_SEARCH]: {},
|
|
[PermissionTypes.PEOPLE_PICKER]: {
|
|
[Permissions.VIEW_USERS]: false,
|
|
[Permissions.VIEW_GROUPS]: false,
|
|
[Permissions.VIEW_ROLES]: false,
|
|
},
|
|
[PermissionTypes.MARKETPLACE]: {
|
|
[Permissions.USE]: false,
|
|
},
|
|
[PermissionTypes.FILE_SEARCH]: {},
|
|
[PermissionTypes.FILE_CITATIONS]: {},
|
|
[PermissionTypes.MCP_SERVERS]: {
|
|
[Permissions.USE]: true,
|
|
[Permissions.CREATE]: false,
|
|
[Permissions.SHARE]: false,
|
|
[Permissions.SHARE_PUBLIC]: false,
|
|
},
|
|
[PermissionTypes.REMOTE_AGENTS]: {
|
|
[Permissions.USE]: false,
|
|
[Permissions.CREATE]: false,
|
|
[Permissions.SHARE]: false,
|
|
[Permissions.SHARE_PUBLIC]: false,
|
|
},
|
|
},
|
|
},
|
|
});
|