diff --git a/api/models/Role.js b/api/models/Role.js index c4abfedad2..07bf5a2ccb 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -99,6 +99,25 @@ async function updateAccessPermissions(roleName, permissionsUpdate) { const updatedPermissions = { ...currentPermissions }; let hasChanges = false; + const unsetFields = {}; + const permissionTypes = Object.keys(permissionsSchema.shape || {}); + for (const permType of permissionTypes) { + if (role[permType] && typeof role[permType] === 'object') { + logger.info( + `Migrating '${roleName}' role from old schema: found '${permType}' at top level`, + ); + + updatedPermissions[permType] = { + ...updatedPermissions[permType], + ...role[permType], + }; + + unsetFields[permType] = 1; + hasChanges = true; + } + } + + // Process the current updates for (const [permissionType, permissions] of Object.entries(updates)) { const currentTypePermissions = currentPermissions[permissionType] || {}; updatedPermissions[permissionType] = { ...currentTypePermissions }; @@ -115,8 +134,36 @@ async function updateAccessPermissions(roleName, permissionsUpdate) { } if (hasChanges) { - // Update only the permissions field. - await updateRoleByName(roleName, { permissions: updatedPermissions }); + const updateObj = { permissions: updatedPermissions }; + + if (Object.keys(unsetFields).length > 0) { + logger.info( + `Unsetting old schema fields for '${roleName}' role: ${Object.keys(unsetFields).join(', ')}`, + ); + + try { + await Role.updateOne( + { name: roleName }, + { + $set: updateObj, + $unset: unsetFields, + }, + ); + + const cache = getLogStores(CacheKeys.ROLES); + const updatedRole = await Role.findOne({ name: roleName }).select('-__v').lean().exec(); + await cache.set(roleName, updatedRole); + + logger.info(`Updated role '${roleName}' and removed old schema fields`); + } catch (updateError) { + logger.error(`Error during role migration update: ${updateError.message}`); + throw updateError; + } + } else { + // Standard update if no migration needed + await updateRoleByName(roleName, updateObj); + } + logger.info(`Updated '${roleName}' role permissions`); } else { logger.info(`No changes needed for '${roleName}' role permissions`); @@ -155,10 +202,90 @@ const initializeRoles = async function () { } }; +/** + * Migrates roles from old schema to new schema structure. + * This can be called directly to fix existing roles. + * + * @param {string} [roleName] - Optional specific role to migrate. If not provided, migrates all roles. + * @returns {Promise} Number of roles migrated. + */ +const migrateRoleSchema = async function (roleName) { + try { + // Get roles to migrate + let roles; + if (roleName) { + const role = await Role.findOne({ name: roleName }); + roles = role ? [role] : []; + } else { + roles = await Role.find({}); + } + + logger.info(`Migrating ${roles.length} roles to new schema structure`); + let migratedCount = 0; + + for (const role of roles) { + const permissionTypes = Object.keys(permissionsSchema.shape || {}); + const unsetFields = {}; + let hasOldSchema = false; + + // Check for old schema fields + for (const permType of permissionTypes) { + if (role[permType] && typeof role[permType] === 'object') { + hasOldSchema = true; + + // Ensure permissions object exists + role.permissions = role.permissions || {}; + + // Migrate permissions from old location to new + role.permissions[permType] = { + ...role.permissions[permType], + ...role[permType], + }; + + // Mark field for removal + unsetFields[permType] = 1; + } + } + + if (hasOldSchema) { + try { + logger.info(`Migrating role '${role.name}' from old schema structure`); + + // Simple update operation + await Role.updateOne( + { _id: role._id }, + { + $set: { permissions: role.permissions }, + $unset: unsetFields, + }, + ); + + // Refresh cache + const cache = getLogStores(CacheKeys.ROLES); + const updatedRole = await Role.findById(role._id).lean().exec(); + await cache.set(role.name, updatedRole); + + migratedCount++; + logger.info(`Migrated role '${role.name}'`); + } catch (error) { + logger.error(`Failed to migrate role '${role.name}': ${error.message}`); + } + } + } + + logger.info(`Migration complete: ${migratedCount} roles migrated`); + return migratedCount; + } catch (error) { + logger.error(`Role schema migration failed: ${error.message}`); + throw error; + } +}; + module.exports = { Role, getRoleByName, initializeRoles, updateRoleByName, updateAccessPermissions, + migrateRoleSchema, }; diff --git a/api/server/routes/roles.js b/api/server/routes/roles.js index e58ebb6fe7..17768c7de6 100644 --- a/api/server/routes/roles.js +++ b/api/server/routes/roles.js @@ -48,7 +48,7 @@ router.put('/:roleName/prompts', checkAdmin, async (req, res) => { const { roleName: _r } = req.params; // TODO: TEMP, use a better parsing for roleName const roleName = _r.toUpperCase(); - /** @type {TRole['PROMPTS']} */ + /** @type {TRole['permissions']['PROMPTS']} */ const updates = req.body; try { @@ -59,10 +59,16 @@ router.put('/:roleName/prompts', checkAdmin, async (req, res) => { return res.status(404).send({ message: 'Role not found' }); } + const currentPermissions = + role.permissions?.[PermissionTypes.PROMPTS] || role[PermissionTypes.PROMPTS] || {}; + const mergedUpdates = { - [PermissionTypes.PROMPTS]: { - ...role[PermissionTypes.PROMPTS], - ...parsedUpdates, + permissions: { + ...role.permissions, + [PermissionTypes.PROMPTS]: { + ...currentPermissions, + ...parsedUpdates, + }, }, }; @@ -81,7 +87,7 @@ router.put('/:roleName/agents', checkAdmin, async (req, res) => { const { roleName: _r } = req.params; // TODO: TEMP, use a better parsing for roleName const roleName = _r.toUpperCase(); - /** @type {TRole['AGENTS']} */ + /** @type {TRole['permissions']['AGENTS']} */ const updates = req.body; try { @@ -92,17 +98,23 @@ router.put('/:roleName/agents', checkAdmin, async (req, res) => { return res.status(404).send({ message: 'Role not found' }); } + const currentPermissions = + role.permissions?.[PermissionTypes.AGENTS] || role[PermissionTypes.AGENTS] || {}; + const mergedUpdates = { - [PermissionTypes.AGENTS]: { - ...role[PermissionTypes.AGENTS], - ...parsedUpdates, + permissions: { + ...role.permissions, + [PermissionTypes.AGENTS]: { + ...currentPermissions, + ...parsedUpdates, + }, }, }; const updatedRole = await updateRoleByName(roleName, mergedUpdates); res.status(200).send(updatedRole); } catch (error) { - return res.status(400).send({ message: 'Invalid prompt permissions.', error: error.errors }); + return res.status(400).send({ message: 'Invalid agent permissions.', error: error.errors }); } }); diff --git a/client/src/components/Prompts/AdminSettings.tsx b/client/src/components/Prompts/AdminSettings.tsx index 7f019fb343..5311e2b37c 100644 --- a/client/src/components/Prompts/AdminSettings.tsx +++ b/client/src/components/Prompts/AdminSettings.tsx @@ -80,10 +80,10 @@ const AdminSettings = () => { const [selectedRole, setSelectedRole] = useState(SystemRoles.USER); const defaultValues = useMemo(() => { - if (roles?.[selectedRole]) { - return roles[selectedRole][PermissionTypes.PROMPTS]; + if (roles?.[selectedRole]?.permissions) { + return roles[selectedRole].permissions[PermissionTypes.PROMPTS]; } - return roleDefaults[selectedRole][PermissionTypes.PROMPTS]; + return roleDefaults[selectedRole].permissions[PermissionTypes.PROMPTS]; }, [roles, selectedRole]); const { @@ -99,10 +99,10 @@ const AdminSettings = () => { }); useEffect(() => { - if (roles?.[selectedRole]?.[PermissionTypes.PROMPTS]) { - reset(roles[selectedRole][PermissionTypes.PROMPTS]); + if (roles?.[selectedRole]?.permissions?.[PermissionTypes.PROMPTS]) { + reset(roles[selectedRole].permissions[PermissionTypes.PROMPTS]); } else { - reset(roleDefaults[selectedRole][PermissionTypes.PROMPTS]); + reset(roleDefaults[selectedRole].permissions[PermissionTypes.PROMPTS]); } }, [roles, selectedRole, reset]); diff --git a/client/src/components/SidePanel/Agents/AdminSettings.tsx b/client/src/components/SidePanel/Agents/AdminSettings.tsx index 5fb13fd045..cd5c0679fc 100644 --- a/client/src/components/SidePanel/Agents/AdminSettings.tsx +++ b/client/src/components/SidePanel/Agents/AdminSettings.tsx @@ -72,10 +72,10 @@ const AdminSettings = () => { const [selectedRole, setSelectedRole] = useState(SystemRoles.USER); const defaultValues = useMemo(() => { - if (roles?.[selectedRole]) { - return roles[selectedRole][PermissionTypes.AGENTS]; + if (roles?.[selectedRole]?.permissions) { + return roles[selectedRole].permissions[PermissionTypes.AGENTS]; } - return roleDefaults[selectedRole][PermissionTypes.AGENTS]; + return roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS]; }, [roles, selectedRole]); const { @@ -91,10 +91,10 @@ const AdminSettings = () => { }); useEffect(() => { - if (roles?.[selectedRole]?.[PermissionTypes.AGENTS]) { - reset(roles[selectedRole][PermissionTypes.AGENTS]); + if (roles?.[selectedRole]?.permissions?.[PermissionTypes.AGENTS]) { + reset(roles[selectedRole].permissions[PermissionTypes.AGENTS]); } else { - reset(roleDefaults[selectedRole][PermissionTypes.AGENTS]); + reset(roleDefaults[selectedRole].permissions[PermissionTypes.AGENTS]); } }, [roles, selectedRole, reset]); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 13635d453c..88b957d5a7 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -482,7 +482,7 @@ "com_ui_agent_editing_allowed": "Other users can already edit this agent", "com_ui_agent_recursion_limit": "Max Agent Steps", "com_ui_agent_recursion_limit_info": "Limits how many steps the agent can take in a run before giving a final response. Default is 25 steps. A step is either an AI API request or a tool usage round. For example, a basic tool interaction takes 3 steps: initial request, tool usage, and follow-up request.", - "com_ui_agent_shared_to_all": "something needs to go here. was empty", + "com_ui_agent_shared_to_all": "Agent is currently shared to all", "com_ui_agent_var": "{{0}} agent", "com_ui_agents": "Agents", "com_ui_agents_allow_create": "Allow creating Agents", diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index ec863fd94f..ca7d64b206 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -74,12 +74,28 @@ export const roleDefaults = defaultRolesSchema.parse({ [SystemRoles.ADMIN]: { name: SystemRoles.ADMIN, permissions: { - [PermissionTypes.PROMPTS]: {}, - [PermissionTypes.BOOKMARKS]: {}, - [PermissionTypes.AGENTS]: {}, - [PermissionTypes.MULTI_CONVO]: {}, - [PermissionTypes.TEMPORARY_CHAT]: {}, - [PermissionTypes.RUN_CODE]: {}, + [PermissionTypes.PROMPTS]: { + [Permissions.SHARED_GLOBAL]: true, + [Permissions.USE]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.BOOKMARKS]: { + [Permissions.USE]: true, + }, + [PermissionTypes.AGENTS]: { + [Permissions.SHARED_GLOBAL]: true, + [Permissions.USE]: true, + [Permissions.CREATE]: true, + }, + [PermissionTypes.MULTI_CONVO]: { + [Permissions.USE]: true, + }, + [PermissionTypes.TEMPORARY_CHAT]: { + [Permissions.USE]: true, + }, + [PermissionTypes.RUN_CODE]: { + [Permissions.USE]: true, + }, }, }, [SystemRoles.USER]: {