🧑‍🤝‍🧑 feat: Add People Picker Permissions Management UI

This commit is contained in:
Danny Avila 2025-08-10 17:42:33 -04:00
parent d82a63642d
commit a434d28579
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
15 changed files with 419 additions and 229 deletions

View file

@ -3,6 +3,7 @@ const {
promptPermissionsSchema,
memoryPermissionsSchema,
agentPermissionsSchema,
peoplePickerPermissionsSchema,
PermissionTypes,
roleDefaults,
SystemRoles,
@ -13,6 +14,76 @@ const { updateRoleByName, getRoleByName } = require('~/models/Role');
const router = express.Router();
router.use(requireJwtAuth);
/**
* Permission configuration mapping
* Maps route paths to their corresponding schemas and permission types
*/
const permissionConfigs = {
prompts: {
schema: promptPermissionsSchema,
permissionType: PermissionTypes.PROMPTS,
errorMessage: 'Invalid prompt permissions.',
},
agents: {
schema: agentPermissionsSchema,
permissionType: PermissionTypes.AGENTS,
errorMessage: 'Invalid agent permissions.',
},
memories: {
schema: memoryPermissionsSchema,
permissionType: PermissionTypes.MEMORIES,
errorMessage: 'Invalid memory permissions.',
},
'people-picker': {
schema: peoplePickerPermissionsSchema,
permissionType: PermissionTypes.PEOPLE_PICKER,
errorMessage: 'Invalid people picker permissions.',
},
};
/**
* Generic handler for updating permissions
* @param {string} permissionKey - The key from permissionConfigs
* @returns {Function} Express route handler
*/
const createPermissionUpdateHandler = (permissionKey) => {
const config = permissionConfigs[permissionKey];
return async (req, res) => {
const { roleName: _r } = req.params;
// TODO: TEMP, use a better parsing for roleName
const roleName = _r.toUpperCase();
const updates = req.body;
try {
const parsedUpdates = config.schema.partial().parse(updates);
const role = await getRoleByName(roleName);
if (!role) {
return res.status(404).send({ message: 'Role not found' });
}
const currentPermissions =
role.permissions?.[config.permissionType] || role[config.permissionType] || {};
const mergedUpdates = {
permissions: {
...role.permissions,
[config.permissionType]: {
...currentPermissions,
...parsedUpdates,
},
},
};
const updatedRole = await updateRoleByName(roleName, mergedUpdates);
res.status(200).send(updatedRole);
} catch (error) {
return res.status(400).send({ message: config.errorMessage, error: error.errors });
}
};
};
/**
* GET /api/roles/:roleName
* Get a specific role by name
@ -45,117 +116,24 @@ router.get('/:roleName', async (req, res) => {
* PUT /api/roles/:roleName/prompts
* Update prompt permissions for a specific role
*/
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['permissions']['PROMPTS']} */
const updates = req.body;
try {
const parsedUpdates = promptPermissionsSchema.partial().parse(updates);
const role = await getRoleByName(roleName);
if (!role) {
return res.status(404).send({ message: 'Role not found' });
}
const currentPermissions =
role.permissions?.[PermissionTypes.PROMPTS] || role[PermissionTypes.PROMPTS] || {};
const mergedUpdates = {
permissions: {
...role.permissions,
[PermissionTypes.PROMPTS]: {
...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 });
}
});
router.put('/:roleName/prompts', checkAdmin, createPermissionUpdateHandler('prompts'));
/**
* PUT /api/roles/:roleName/agents
* Update agent permissions for a specific role
*/
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['permissions']['AGENTS']} */
const updates = req.body;
try {
const parsedUpdates = agentPermissionsSchema.partial().parse(updates);
const role = await getRoleByName(roleName);
if (!role) {
return res.status(404).send({ message: 'Role not found' });
}
const currentPermissions =
role.permissions?.[PermissionTypes.AGENTS] || role[PermissionTypes.AGENTS] || {};
const mergedUpdates = {
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 agent permissions.', error: error.errors });
}
});
router.put('/:roleName/agents', checkAdmin, createPermissionUpdateHandler('agents'));
/**
* PUT /api/roles/:roleName/memories
* Update memory permissions for a specific role
*/
router.put('/:roleName/memories', checkAdmin, async (req, res) => {
const { roleName: _r } = req.params;
// TODO: TEMP, use a better parsing for roleName
const roleName = _r.toUpperCase();
/** @type {TRole['permissions']['MEMORIES']} */
const updates = req.body;
router.put('/:roleName/memories', checkAdmin, createPermissionUpdateHandler('memories'));
try {
const parsedUpdates = memoryPermissionsSchema.partial().parse(updates);
const role = await getRoleByName(roleName);
if (!role) {
return res.status(404).send({ message: 'Role not found' });
}
const currentPermissions =
role.permissions?.[PermissionTypes.MEMORIES] || role[PermissionTypes.MEMORIES] || {};
const mergedUpdates = {
permissions: {
...role.permissions,
[PermissionTypes.MEMORIES]: {
...currentPermissions,
...parsedUpdates,
},
},
};
const updatedRole = await updateRoleByName(roleName, mergedUpdates);
res.status(200).send(updatedRole);
} catch (error) {
return res.status(400).send({ message: 'Invalid memory permissions.', error: error.errors });
}
});
/**
* PUT /api/roles/:roleName/people-picker
* Update people picker permissions for a specific role
*/
router.put('/:roleName/people-picker', checkAdmin, createPermissionUpdateHandler('people-picker'));
module.exports = router;

View file

@ -94,109 +94,71 @@ describe('AppService interface configuration', () => {
mockLoadCustomConfig.mockResolvedValue({
interface: {
peoplePicker: {
admin: {
users: true,
groups: true,
roles: true,
},
user: {
users: false,
groups: false,
roles: false,
},
users: true,
groups: true,
roles: true,
},
},
});
loadDefaultInterface.mockResolvedValue({
peoplePicker: {
admin: {
users: true,
groups: true,
roles: true,
},
user: {
users: false,
groups: false,
roles: false,
},
users: true,
groups: true,
roles: true,
},
});
await AppService(app);
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
expect(app.locals.interfaceConfig.peoplePicker.admin).toMatchObject({
expect(app.locals.interfaceConfig.peoplePicker).toMatchObject({
users: true,
groups: true,
roles: true,
});
expect(app.locals.interfaceConfig.peoplePicker.user).toMatchObject({
users: false,
groups: false,
roles: false,
});
expect(loadDefaultInterface).toHaveBeenCalled();
});
it('should handle mixed peoplePicker permissions for roles', async () => {
it('should handle mixed peoplePicker permissions', async () => {
mockLoadCustomConfig.mockResolvedValue({
interface: {
peoplePicker: {
admin: {
users: true,
groups: true,
roles: false,
},
user: {
users: true,
groups: false,
roles: true,
},
users: true,
groups: false,
roles: true,
},
},
});
loadDefaultInterface.mockResolvedValue({
peoplePicker: {
admin: {
users: true,
groups: true,
roles: false,
},
user: {
users: true,
groups: false,
roles: true,
},
users: true,
groups: false,
roles: true,
},
});
await AppService(app);
expect(app.locals.interfaceConfig.peoplePicker.admin.roles).toBe(false);
expect(app.locals.interfaceConfig.peoplePicker.user.roles).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(false);
expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true);
});
it('should set default peoplePicker roles permissions when not provided', async () => {
it('should set default peoplePicker permissions when not provided', async () => {
mockLoadCustomConfig.mockResolvedValue({});
loadDefaultInterface.mockResolvedValue({
peoplePicker: {
admin: {
users: true,
groups: true,
roles: true,
},
user: {
users: false,
groups: false,
roles: false,
},
users: true,
groups: true,
roles: true,
},
});
await AppService(app);
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
expect(app.locals.interfaceConfig.peoplePicker.admin.roles).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.user.roles).toBe(false);
expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true);
});
});

View file

@ -970,20 +970,13 @@ describe('AppService updating app.locals and issuing warnings', () => {
expect(app.locals.ocr.mistralModel).toEqual('mistral-medium');
});
it('should correctly configure peoplePicker with roles permission when specified', async () => {
it('should correctly configure peoplePicker permissions when specified', async () => {
const mockConfig = {
interface: {
peoplePicker: {
admin: {
users: true,
groups: true,
roles: true,
},
user: {
users: false,
groups: false,
roles: true,
},
users: true,
groups: true,
roles: true,
},
},
};
@ -993,21 +986,16 @@ describe('AppService updating app.locals and issuing warnings', () => {
const app = { locals: {} };
await AppService(app);
// Check that interface config includes the roles permission
// Check that interface config includes the permissions
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
expect(app.locals.interfaceConfig.peoplePicker.admin).toMatchObject({
expect(app.locals.interfaceConfig.peoplePicker).toMatchObject({
users: true,
groups: true,
roles: true,
});
expect(app.locals.interfaceConfig.peoplePicker.user).toMatchObject({
users: false,
groups: false,
roles: true,
});
});
it('should use default peoplePicker roles permissions when not specified', async () => {
it('should use default peoplePicker permissions when not specified', async () => {
const mockConfig = {
interface: {
// No peoplePicker configuration
@ -1019,9 +1007,10 @@ describe('AppService updating app.locals and issuing warnings', () => {
const app = { locals: {} };
await AppService(app);
// Check that default roles permissions are applied
// Check that default permissions are applied
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
expect(app.locals.interfaceConfig.peoplePicker.admin.roles).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.user.roles).toBe(false);
expect(app.locals.interfaceConfig.peoplePicker.users).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.groups).toBe(true);
expect(app.locals.interfaceConfig.peoplePicker.roles).toBe(true);
});
});

View file

@ -54,16 +54,9 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
fileCitations: interfaceConfig?.fileCitations ?? defaults.fileCitations,
customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome,
peoplePicker: {
admin: {
users: interfaceConfig?.peoplePicker?.admin?.users ?? defaults.peoplePicker?.admin.users,
groups: interfaceConfig?.peoplePicker?.admin?.groups ?? defaults.peoplePicker?.admin.groups,
roles: interfaceConfig?.peoplePicker?.admin?.roles ?? defaults.peoplePicker?.admin.roles,
},
user: {
users: interfaceConfig?.peoplePicker?.user?.users ?? defaults.peoplePicker?.user.users,
groups: interfaceConfig?.peoplePicker?.user?.groups ?? defaults.peoplePicker?.user.groups,
roles: interfaceConfig?.peoplePicker?.user?.roles ?? defaults.peoplePicker?.user.roles,
},
users: interfaceConfig?.peoplePicker?.users ?? defaults.peoplePicker?.users,
groups: interfaceConfig?.peoplePicker?.groups ?? defaults.peoplePicker?.groups,
roles: interfaceConfig?.peoplePicker?.roles ?? defaults.peoplePicker?.roles,
},
marketplace: {
admin: {
@ -88,9 +81,12 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch },
[PermissionTypes.PEOPLE_PICKER]: {
[Permissions.VIEW_USERS]: loadedInterface.peoplePicker.user?.users,
[Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.user?.groups,
[Permissions.VIEW_ROLES]: loadedInterface.peoplePicker.user?.roles,
[Permissions.VIEW_USERS]:
roleName === SystemRoles.USER ? false : loadedInterface.peoplePicker?.users,
[Permissions.VIEW_GROUPS]:
roleName === SystemRoles.USER ? false : loadedInterface.peoplePicker?.groups,
[Permissions.VIEW_ROLES]:
roleName === SystemRoles.USER ? false : loadedInterface.peoplePicker?.roles,
},
[PermissionTypes.MARKETPLACE]: {
[Permissions.USE]: loadedInterface.marketplace.user?.use,
@ -111,9 +107,9 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
[PermissionTypes.RUN_CODE]: { [Permissions.USE]: loadedInterface.runCode },
[PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: loadedInterface.webSearch },
[PermissionTypes.PEOPLE_PICKER]: {
[Permissions.VIEW_USERS]: loadedInterface.peoplePicker.admin?.users,
[Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.admin?.groups,
[Permissions.VIEW_ROLES]: loadedInterface.peoplePicker.admin?.roles,
[Permissions.VIEW_USERS]: loadedInterface.peoplePicker?.users,
[Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker?.groups,
[Permissions.VIEW_ROLES]: loadedInterface.peoplePicker?.roles,
},
[PermissionTypes.MARKETPLACE]: {
[Permissions.USE]: loadedInterface.marketplace.admin?.use,