🪺 fix: Update Role Handling due to New Schema Shape (#6774)

* 📝 fix: Update translation for shared agent message in English locale

* 🪺 fix: Migrate role schema to new nested structure and update permissions handling where missed
This commit is contained in:
Danny Avila 2025-04-07 14:48:11 -04:00 committed by GitHub
parent 175cfe8ffb
commit 4afab52fc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 185 additions and 30 deletions

View file

@ -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>} 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,
};

View file

@ -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 });
}
});

View file

@ -80,10 +80,10 @@ const AdminSettings = () => {
const [selectedRole, setSelectedRole] = useState<SystemRoles>(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]);

View file

@ -72,10 +72,10 @@ const AdminSettings = () => {
const [selectedRole, setSelectedRole] = useState<SystemRoles>(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]);

View file

@ -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",

View file

@ -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]: {