mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-24 04:10:15 +01:00
🛂 feat: Role as Permission Principal Type
WIP: Role as Permission Principal Type WIP: add user role check optimization to user principal check, update type comparisons WIP: cover edge cases for string vs ObjectId handling in permission granting and checking chore: Update people picker access middleware to use PrincipalType constants feat: Enhance people picker access control to include roles permissions chore: add missing default role schema values for people picker perms, cleanup typing feat: Enhance PeoplePicker component with role-specific UI and localization updates chore: Add missing `VIEW_ROLES` permission to role schema
This commit is contained in:
parent
28d63dab71
commit
39346d6b8e
49 changed files with 2879 additions and 258 deletions
|
|
@ -72,7 +72,7 @@ const updateResourcePermissions = async (req, res) => {
|
|||
// Add public permission if enabled
|
||||
if (isPublic && publicAccessRoleId) {
|
||||
updatedPrincipals.push({
|
||||
type: 'public',
|
||||
type: PrincipalType.PUBLIC,
|
||||
id: null,
|
||||
accessRoleId: publicAccessRoleId,
|
||||
});
|
||||
|
|
@ -97,11 +97,13 @@ const updateResourcePermissions = async (req, res) => {
|
|||
try {
|
||||
let principalId;
|
||||
|
||||
if (principal.type === 'public') {
|
||||
if (principal.type === PrincipalType.PUBLIC) {
|
||||
principalId = null; // Public principals don't need database records
|
||||
} else if (principal.type === 'user') {
|
||||
} else if (principal.type === PrincipalType.ROLE) {
|
||||
principalId = principal.id; // Role principals use role name as ID
|
||||
} else if (principal.type === PrincipalType.USER) {
|
||||
principalId = await ensurePrincipalExists(principal);
|
||||
} else if (principal.type === 'group') {
|
||||
} else if (principal.type === PrincipalType.GROUP) {
|
||||
// Pass authContext to enable member fetching for Entra ID groups when available
|
||||
principalId = await ensureGroupPrincipalExists(principal, authContext);
|
||||
} else {
|
||||
|
|
@ -137,7 +139,7 @@ const updateResourcePermissions = async (req, res) => {
|
|||
// If public is disabled, add public to revoked list
|
||||
if (!isPublic) {
|
||||
revokedPrincipals.push({
|
||||
type: 'public',
|
||||
type: PrincipalType.PUBLIC,
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
|
|
@ -263,6 +265,16 @@ const getResourcePermissions = async (req, res) => {
|
|||
idOnTheSource: result.groupInfo.idOnTheSource || result.groupInfo._id.toString(),
|
||||
accessRoleId: result.accessRoleId,
|
||||
});
|
||||
} else if (result.principalType === PrincipalType.ROLE) {
|
||||
principals.push({
|
||||
type: PrincipalType.ROLE,
|
||||
/** Role name as ID */
|
||||
id: result.principalId,
|
||||
/** Display the role name */
|
||||
name: result.principalId,
|
||||
description: `System role: ${result.principalId}`,
|
||||
accessRoleId: result.accessRoleId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -328,6 +340,7 @@ const getUserEffectivePermissions = async (req, res) => {
|
|||
|
||||
const permissionBits = await getEffectivePermissions({
|
||||
userId,
|
||||
role: req.user.role,
|
||||
resourceType,
|
||||
resourceId,
|
||||
});
|
||||
|
|
@ -366,7 +379,9 @@ const searchPrincipals = async (req, res) => {
|
|||
}
|
||||
|
||||
const searchLimit = Math.min(Math.max(1, parseInt(limit) || 10), 50);
|
||||
const typeFilter = ['user', 'group'].includes(type) ? type : null;
|
||||
const typeFilter = [PrincipalType.USER, PrincipalType.GROUP, PrincipalType.ROLE].includes(type)
|
||||
? type
|
||||
: null;
|
||||
|
||||
const localResults = await searchLocalPrincipals(query.trim(), searchLimit, typeFilter);
|
||||
let allPrincipals = [...localResults];
|
||||
|
|
|
|||
|
|
@ -444,6 +444,7 @@ const getListAgentsHandler = async (req, res) => {
|
|||
// Get agent IDs the user has VIEW access to via ACL
|
||||
const accessibleIds = await findAccessibleResources({
|
||||
userId,
|
||||
role: req.user.role,
|
||||
resourceType: ResourceType.AGENT,
|
||||
requiredPermissions: requiredPermission,
|
||||
});
|
||||
|
|
@ -499,7 +500,7 @@ const uploadAgentAvatarHandler = async (req, res) => {
|
|||
return res.status(404).json({ error: 'Agent not found' });
|
||||
}
|
||||
|
||||
const isAuthor = existingAgent.author.toString() === req.user.id;
|
||||
const isAuthor = existingAgent.author.toString() === req.user.id.toString();
|
||||
const hasEditPermission = existingAgent.isCollaborative || isAdmin || isAuthor;
|
||||
|
||||
if (!hasEditPermission) {
|
||||
|
|
@ -607,7 +608,7 @@ const revertAgentVersionHandler = async (req, res) => {
|
|||
return res.status(404).json({ error: 'Agent not found' });
|
||||
}
|
||||
|
||||
const isAuthor = existingAgent.author.toString() === req.user.id;
|
||||
const isAuthor = existingAgent.author.toString() === req.user.id.toString();
|
||||
const hasEditPermission = existingAgent.isCollaborative || isAdmin || isAuthor;
|
||||
|
||||
if (!hasEditPermission) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ describe('canAccessAgentResource middleware', () => {
|
|||
});
|
||||
|
||||
req = {
|
||||
user: { id: testUser._id.toString(), role: 'test-role' },
|
||||
user: { id: testUser._id, role: testUser.role },
|
||||
params: {},
|
||||
};
|
||||
res = {
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ const canAccessResource = (options) => {
|
|||
// Check permissions using PermissionService with ObjectId
|
||||
const hasPermission = await checkPermission({
|
||||
userId,
|
||||
role: req.user.role,
|
||||
resourceType,
|
||||
resourceId,
|
||||
requiredPermission,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const { getFiles } = require('~/models/File');
|
|||
* Checks if user has access to a file through agent permissions
|
||||
* Files inherit permissions from agents - if you can view the agent, you can access its files
|
||||
*/
|
||||
const checkAgentBasedFileAccess = async (userId, fileId) => {
|
||||
const checkAgentBasedFileAccess = async ({ userId, role, fileId }) => {
|
||||
try {
|
||||
// Find agents that have this file in their tool_resources
|
||||
const agentsWithFile = await getAgent({
|
||||
|
|
@ -35,6 +35,7 @@ const checkAgentBasedFileAccess = async (userId, fileId) => {
|
|||
try {
|
||||
const permissions = await getEffectivePermissions({
|
||||
userId,
|
||||
role,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: agent._id || agent.id,
|
||||
});
|
||||
|
|
@ -67,7 +68,7 @@ const fileAccess = async (req, res, next) => {
|
|||
try {
|
||||
const fileId = req.params.file_id;
|
||||
const userId = req.user?.id;
|
||||
|
||||
const userRole = req.user?.role;
|
||||
if (!fileId) {
|
||||
return res.status(400).json({
|
||||
error: 'Bad Request',
|
||||
|
|
@ -98,7 +99,7 @@ const fileAccess = async (req, res, next) => {
|
|||
}
|
||||
|
||||
// Check agent-based access (file inherits agent permissions)
|
||||
const hasAgentAccess = await checkAgentBasedFileAccess(userId, fileId);
|
||||
const hasAgentAccess = await checkAgentBasedFileAccess({ userId, role: userRole, fileId });
|
||||
if (hasAgentAccess) {
|
||||
req.fileAccess = { file };
|
||||
return next();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||
const { PrincipalType, PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||
const { getRoleByName } = require('~/models/Role');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
|
|
@ -7,7 +7,8 @@ const { logger } = require('~/config');
|
|||
* Checks specific permission based on the 'type' query parameter:
|
||||
* - type=user: requires VIEW_USERS permission
|
||||
* - type=group: requires VIEW_GROUPS permission
|
||||
* - no type (mixed search): requires either VIEW_USERS OR VIEW_GROUPS
|
||||
* - type=role: requires VIEW_ROLES permission
|
||||
* - no type (mixed search): requires either VIEW_USERS OR VIEW_GROUPS OR VIEW_ROLES
|
||||
*/
|
||||
const checkPeoplePickerAccess = async (req, res, next) => {
|
||||
try {
|
||||
|
|
@ -31,29 +32,38 @@ const checkPeoplePickerAccess = async (req, res, next) => {
|
|||
const peoplePickerPerms = role.permissions[PermissionTypes.PEOPLE_PICKER] || {};
|
||||
const canViewUsers = peoplePickerPerms[Permissions.VIEW_USERS] === true;
|
||||
const canViewGroups = peoplePickerPerms[Permissions.VIEW_GROUPS] === true;
|
||||
const canViewRoles = peoplePickerPerms[Permissions.VIEW_ROLES] === true;
|
||||
|
||||
if (type === 'user') {
|
||||
if (!canViewUsers) {
|
||||
return res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for users',
|
||||
});
|
||||
}
|
||||
} else if (type === 'group') {
|
||||
if (!canViewGroups) {
|
||||
return res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for groups',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (!canViewUsers || !canViewGroups) {
|
||||
return res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for both users and groups',
|
||||
});
|
||||
}
|
||||
const permissionChecks = {
|
||||
[PrincipalType.USER]: {
|
||||
hasPermission: canViewUsers,
|
||||
message: 'Insufficient permissions to search for users',
|
||||
},
|
||||
[PrincipalType.GROUP]: {
|
||||
hasPermission: canViewGroups,
|
||||
message: 'Insufficient permissions to search for groups',
|
||||
},
|
||||
[PrincipalType.ROLE]: {
|
||||
hasPermission: canViewRoles,
|
||||
message: 'Insufficient permissions to search for roles',
|
||||
},
|
||||
};
|
||||
|
||||
const check = permissionChecks[type];
|
||||
if (check && !check.hasPermission) {
|
||||
return res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: check.message,
|
||||
});
|
||||
}
|
||||
|
||||
if (!type && !canViewUsers && !canViewGroups && !canViewRoles) {
|
||||
return res.status(403).json({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for users, groups, or roles',
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
|
|
|
|||
250
api/server/middleware/checkPeoplePickerAccess.spec.js
Normal file
250
api/server/middleware/checkPeoplePickerAccess.spec.js
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
const { PrincipalType, PermissionTypes, Permissions } = require('librechat-data-provider');
|
||||
const { checkPeoplePickerAccess } = require('./checkPeoplePickerAccess');
|
||||
const { getRoleByName } = require('~/models/Role');
|
||||
const { logger } = require('~/config');
|
||||
|
||||
jest.mock('~/models/Role');
|
||||
jest.mock('~/config', () => ({
|
||||
logger: {
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('checkPeoplePickerAccess', () => {
|
||||
let req, res, next;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
user: { id: 'user123', role: 'USER' },
|
||||
query: {},
|
||||
};
|
||||
res = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
};
|
||||
next = jest.fn();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return 401 if user is not authenticated', async () => {
|
||||
req.user = null;
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Unauthorized',
|
||||
message: 'Authentication required',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 403 if role has no permissions', async () => {
|
||||
getRoleByName.mockResolvedValue(null);
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Forbidden',
|
||||
message: 'No permissions configured for user role',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow access when searching for users with VIEW_USERS permission', async () => {
|
||||
req.query.type = PrincipalType.USER;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: true,
|
||||
[Permissions.VIEW_GROUPS]: false,
|
||||
[Permissions.VIEW_ROLES]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deny access when searching for users without VIEW_USERS permission', async () => {
|
||||
req.query.type = PrincipalType.USER;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: false,
|
||||
[Permissions.VIEW_GROUPS]: true,
|
||||
[Permissions.VIEW_ROLES]: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for users',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow access when searching for groups with VIEW_GROUPS permission', async () => {
|
||||
req.query.type = PrincipalType.GROUP;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: false,
|
||||
[Permissions.VIEW_GROUPS]: true,
|
||||
[Permissions.VIEW_ROLES]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deny access when searching for groups without VIEW_GROUPS permission', async () => {
|
||||
req.query.type = PrincipalType.GROUP;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: true,
|
||||
[Permissions.VIEW_GROUPS]: false,
|
||||
[Permissions.VIEW_ROLES]: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for groups',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow access when searching for roles with VIEW_ROLES permission', async () => {
|
||||
req.query.type = PrincipalType.ROLE;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: false,
|
||||
[Permissions.VIEW_GROUPS]: false,
|
||||
[Permissions.VIEW_ROLES]: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deny access when searching for roles without VIEW_ROLES permission', async () => {
|
||||
req.query.type = PrincipalType.ROLE;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: true,
|
||||
[Permissions.VIEW_GROUPS]: true,
|
||||
[Permissions.VIEW_ROLES]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for roles',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow mixed search when user has at least one permission', async () => {
|
||||
// No type specified = mixed search
|
||||
req.query.type = undefined;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: false,
|
||||
[Permissions.VIEW_GROUPS]: false,
|
||||
[Permissions.VIEW_ROLES]: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deny mixed search when user has no permissions', async () => {
|
||||
// No type specified = mixed search
|
||||
req.query.type = undefined;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {
|
||||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: false,
|
||||
[Permissions.VIEW_GROUPS]: false,
|
||||
[Permissions.VIEW_ROLES]: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for users, groups, or roles',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const error = new Error('Database error');
|
||||
getRoleByName.mockRejectedValue(error);
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'[checkPeoplePickerAccess][user123] checkPeoplePickerAccess error for req.query.type = undefined',
|
||||
error,
|
||||
);
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Internal Server Error',
|
||||
message: 'Failed to check permissions',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle missing permissions object gracefully', async () => {
|
||||
req.query.type = PrincipalType.USER;
|
||||
getRoleByName.mockResolvedValue({
|
||||
permissions: {}, // No PEOPLE_PICKER permissions
|
||||
});
|
||||
|
||||
await checkPeoplePickerAccess(req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Forbidden',
|
||||
message: 'Insufficient permissions to search for users',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -37,6 +37,7 @@ router.get('/', async (req, res) => {
|
|||
const userId = req.user.id;
|
||||
const editableAgentObjectIds = await findAccessibleResources({
|
||||
userId,
|
||||
role: req.user.role,
|
||||
resourceType: ResourceType.AGENT,
|
||||
requiredPermissions: PermissionBits.EDIT,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ router.get('/agent/:agent_id', async (req, res) => {
|
|||
if (agent.author.toString() !== userId) {
|
||||
const hasEditPermission = await checkPermission({
|
||||
userId,
|
||||
role: req.user.role,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: agent._id,
|
||||
requiredPermission: PermissionBits.EDIT,
|
||||
|
|
@ -152,7 +153,7 @@ router.delete('/', async (req, res) => {
|
|||
const nonOwnedFiles = [];
|
||||
|
||||
for (const file of dbFiles) {
|
||||
if (file.user.toString() === req.user.id) {
|
||||
if (file.user.toString() === req.user.id.toString()) {
|
||||
ownedFiles.push(file);
|
||||
} else {
|
||||
nonOwnedFiles.push(file);
|
||||
|
|
@ -176,11 +177,12 @@ router.delete('/', async (req, res) => {
|
|||
|
||||
if (req.body.agent_id && nonOwnedFiles.length > 0) {
|
||||
const nonOwnedFileIds = nonOwnedFiles.map((f) => f.file_id);
|
||||
const accessMap = await hasAccessToFilesViaAgent(
|
||||
req.user.id,
|
||||
nonOwnedFileIds,
|
||||
req.body.agent_id,
|
||||
);
|
||||
const accessMap = await hasAccessToFilesViaAgent({
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
fileIds: nonOwnedFileIds,
|
||||
agentId: req.body.agent_id,
|
||||
});
|
||||
|
||||
for (const file of nonOwnedFiles) {
|
||||
if (accessMap.get(file.file_id)) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ const mongoose = require('mongoose');
|
|||
const { v4: uuidv4 } = require('uuid');
|
||||
const { createMethods } = require('@librechat/data-schemas');
|
||||
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||
const { AccessRoleIds, ResourceType, PrincipalType } = require('librechat-data-provider');
|
||||
const {
|
||||
SystemRoles,
|
||||
ResourceType,
|
||||
AccessRoleIds,
|
||||
PrincipalType,
|
||||
} = require('librechat-data-provider');
|
||||
const { createAgent } = require('~/models/Agent');
|
||||
const { createFile } = require('~/models/File');
|
||||
|
||||
|
|
@ -95,9 +100,11 @@ describe('File Routes - Delete with Agent Access', () => {
|
|||
app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Mock authentication middleware
|
||||
app.use((req, res, next) => {
|
||||
req.user = { id: otherUserId ? otherUserId.toString() : 'default-user' };
|
||||
req.user = {
|
||||
id: otherUserId || 'default-user',
|
||||
role: SystemRoles.USER,
|
||||
};
|
||||
req.app = { locals: {} };
|
||||
next();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ router.get('/all', async (req, res) => {
|
|||
// Get promptGroup IDs the user has VIEW access to via ACL
|
||||
const accessibleIds = await findAccessibleResources({
|
||||
userId,
|
||||
role: req.user.role,
|
||||
resourceType: ResourceType.PROMPTGROUP,
|
||||
requiredPermissions: PermissionBits.VIEW,
|
||||
});
|
||||
|
|
@ -130,6 +131,7 @@ router.get('/groups', async (req, res) => {
|
|||
// Get promptGroup IDs the user has VIEW access to via ACL
|
||||
const accessibleIds = await findAccessibleResources({
|
||||
userId,
|
||||
role: req.user.role,
|
||||
resourceType: ResourceType.PROMPTGROUP,
|
||||
requiredPermissions: PermissionBits.VIEW,
|
||||
});
|
||||
|
|
@ -334,6 +336,7 @@ router.get('/', async (req, res) => {
|
|||
if (groupId) {
|
||||
const permissions = await getEffectivePermissions({
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
resourceType: ResourceType.PROMPTGROUP,
|
||||
resourceId: groupId,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -214,8 +214,6 @@ describe('Prompt Routes - ACL Permissions', () => {
|
|||
console.log('Console errors:', consoleErrorSpy.mock.calls);
|
||||
}
|
||||
|
||||
console.log('POST response:', response.body);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.prompt).toBeDefined();
|
||||
expect(response.body.prompt.prompt).toBe(promptData.prompt.prompt);
|
||||
|
|
@ -303,8 +301,8 @@ describe('Prompt Routes - ACL Permissions', () => {
|
|||
grantedBy: testUsers.owner._id,
|
||||
});
|
||||
|
||||
const response = await request(app).get(`/api/prompts/${testPrompt._id}`).expect(200);
|
||||
|
||||
const response = await request(app).get(`/api/prompts/${testPrompt._id}`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body._id).toBe(testPrompt._id.toString());
|
||||
expect(response.body.prompt).toBe(testPrompt.prompt);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,4 +89,114 @@ describe('AppService interface configuration', () => {
|
|||
expect(app.locals.interfaceConfig.bookmarks).toBe(false);
|
||||
expect(loadDefaultInterface).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should correctly configure peoplePicker permissions including roles', async () => {
|
||||
mockLoadCustomConfig.mockResolvedValue({
|
||||
interface: {
|
||||
peoplePicker: {
|
||||
admin: {
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: true,
|
||||
},
|
||||
user: {
|
||||
users: false,
|
||||
groups: false,
|
||||
roles: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
loadDefaultInterface.mockResolvedValue({
|
||||
peoplePicker: {
|
||||
admin: {
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: true,
|
||||
},
|
||||
user: {
|
||||
users: false,
|
||||
groups: false,
|
||||
roles: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await AppService(app);
|
||||
|
||||
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
|
||||
expect(app.locals.interfaceConfig.peoplePicker.admin).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 () => {
|
||||
mockLoadCustomConfig.mockResolvedValue({
|
||||
interface: {
|
||||
peoplePicker: {
|
||||
admin: {
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: false,
|
||||
},
|
||||
user: {
|
||||
users: true,
|
||||
groups: false,
|
||||
roles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
loadDefaultInterface.mockResolvedValue({
|
||||
peoplePicker: {
|
||||
admin: {
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: false,
|
||||
},
|
||||
user: {
|
||||
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);
|
||||
});
|
||||
|
||||
it('should set default peoplePicker roles permissions when not provided', async () => {
|
||||
mockLoadCustomConfig.mockResolvedValue({});
|
||||
loadDefaultInterface.mockResolvedValue({
|
||||
peoplePicker: {
|
||||
admin: {
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: true,
|
||||
},
|
||||
user: {
|
||||
users: false,
|
||||
groups: false,
|
||||
roles: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -969,4 +969,59 @@ describe('AppService updating app.locals and issuing warnings', () => {
|
|||
expect(app.locals.ocr.strategy).toEqual('mistral_ocr');
|
||||
expect(app.locals.ocr.mistralModel).toEqual('mistral-medium');
|
||||
});
|
||||
|
||||
it('should correctly configure peoplePicker with roles permission when specified', async () => {
|
||||
const mockConfig = {
|
||||
interface: {
|
||||
peoplePicker: {
|
||||
admin: {
|
||||
users: true,
|
||||
groups: true,
|
||||
roles: true,
|
||||
},
|
||||
user: {
|
||||
users: false,
|
||||
groups: false,
|
||||
roles: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||
|
||||
const app = { locals: {} };
|
||||
await AppService(app);
|
||||
|
||||
// Check that interface config includes the roles permission
|
||||
expect(app.locals.interfaceConfig.peoplePicker).toBeDefined();
|
||||
expect(app.locals.interfaceConfig.peoplePicker.admin).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 () => {
|
||||
const mockConfig = {
|
||||
interface: {
|
||||
// No peoplePicker configuration
|
||||
},
|
||||
};
|
||||
|
||||
require('./Config/loadCustomConfig').mockImplementationOnce(() => Promise.resolve(mockConfig));
|
||||
|
||||
const app = { locals: {} };
|
||||
await AppService(app);
|
||||
|
||||
// Check that default roles 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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -172,7 +172,12 @@ const primeFiles = async (options, apiKey) => {
|
|||
// Filter by access if user and agent are provided
|
||||
let dbFiles;
|
||||
if (req?.user?.id && agentId) {
|
||||
dbFiles = await filterFilesByAgentAccess(allFiles, req.user.id, agentId);
|
||||
dbFiles = await filterFilesByAgentAccess({
|
||||
files: allFiles,
|
||||
userId: req.user.id,
|
||||
role: req.user.role,
|
||||
agentId,
|
||||
});
|
||||
} else {
|
||||
dbFiles = allFiles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,14 @@ const { getAgent } = require('~/models/Agent');
|
|||
|
||||
/**
|
||||
* Checks if a user has access to multiple files through a shared agent (batch operation)
|
||||
* @param {string} userId - The user ID to check access for
|
||||
* @param {string[]} fileIds - Array of file IDs to check
|
||||
* @param {string} agentId - The agent ID that might grant access
|
||||
* @param {Object} params - Parameters object
|
||||
* @param {string} params.userId - The user ID to check access for
|
||||
* @param {string} [params.role] - Optional user role to avoid DB query
|
||||
* @param {string[]} params.fileIds - Array of file IDs to check
|
||||
* @param {string} params.agentId - The agent ID that might grant access
|
||||
* @returns {Promise<Map<string, boolean>>} Map of fileId to access status
|
||||
*/
|
||||
const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
|
||||
const hasAccessToFilesViaAgent = async ({ userId, role, fileIds, agentId }) => {
|
||||
const accessMap = new Map();
|
||||
|
||||
// Initialize all files as no access
|
||||
|
|
@ -24,7 +26,7 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
|
|||
}
|
||||
|
||||
// Check if user is the author - if so, grant access to all files
|
||||
if (agent.author.toString() === userId) {
|
||||
if (agent.author.toString() === userId.toString()) {
|
||||
fileIds.forEach((fileId) => accessMap.set(fileId, true));
|
||||
return accessMap;
|
||||
}
|
||||
|
|
@ -32,6 +34,7 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
|
|||
// Check if user has at least VIEW permission on the agent
|
||||
const hasViewPermission = await checkPermission({
|
||||
userId,
|
||||
role,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: agent._id,
|
||||
requiredPermission: PermissionBits.VIEW,
|
||||
|
|
@ -44,6 +47,7 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
|
|||
// Check if user has EDIT permission (which would indicate collaborative access)
|
||||
const hasEditPermission = await checkPermission({
|
||||
userId,
|
||||
role,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: agent._id,
|
||||
requiredPermission: PermissionBits.EDIT,
|
||||
|
|
@ -81,12 +85,14 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => {
|
|||
|
||||
/**
|
||||
* Filter files based on user access through agents
|
||||
* @param {Array<MongoFile>} files - Array of file documents
|
||||
* @param {string} userId - User ID for access control
|
||||
* @param {string} agentId - Agent ID that might grant access to files
|
||||
* @param {Object} params - Parameters object
|
||||
* @param {Array<MongoFile>} params.files - Array of file documents
|
||||
* @param {string} params.userId - User ID for access control
|
||||
* @param {string} [params.role] - Optional user role to avoid DB query
|
||||
* @param {string} params.agentId - Agent ID that might grant access to files
|
||||
* @returns {Promise<Array<MongoFile>>} Filtered array of accessible files
|
||||
*/
|
||||
const filterFilesByAgentAccess = async (files, userId, agentId) => {
|
||||
const filterFilesByAgentAccess = async ({ files, userId, role, agentId }) => {
|
||||
if (!userId || !agentId || !files || files.length === 0) {
|
||||
return files;
|
||||
}
|
||||
|
|
@ -96,7 +102,7 @@ const filterFilesByAgentAccess = async (files, userId, agentId) => {
|
|||
const ownedFiles = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (file.user && file.user.toString() === userId) {
|
||||
if (file.user && file.user.toString() === userId.toString()) {
|
||||
ownedFiles.push(file);
|
||||
} else {
|
||||
filesToCheck.push(file);
|
||||
|
|
@ -109,7 +115,7 @@ const filterFilesByAgentAccess = async (files, userId, agentId) => {
|
|||
|
||||
// Batch check access for all non-owned files
|
||||
const fileIds = filesToCheck.map((f) => f.file_id);
|
||||
const accessMap = await hasAccessToFilesViaAgent(userId, fileIds, agentId);
|
||||
const accessMap = await hasAccessToFilesViaAgent({ userId, role, fileIds, agentId });
|
||||
|
||||
// Filter files based on access
|
||||
const accessibleFiles = filesToCheck.filter((file) => accessMap.get(file.file_id));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider');
|
||||
const { getTransactionSupport, logger } = require('@librechat/data-schemas');
|
||||
const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider');
|
||||
const {
|
||||
entraIdPrincipalFeatureEnabled,
|
||||
getUserOwnedEntraGroups,
|
||||
|
|
@ -46,8 +46,8 @@ const validateResourceType = (resourceType) => {
|
|||
/**
|
||||
* Grant a permission to a principal for a resource using a role
|
||||
* @param {Object} params - Parameters for granting role-based permission
|
||||
* @param {string} params.principalType - 'user', 'group', or 'public'
|
||||
* @param {string|mongoose.Types.ObjectId|null} params.principalId - The ID of the principal (null for 'public')
|
||||
* @param {string} params.principalType - PrincipalType.USER, PrincipalType.GROUP, or PrincipalType.PUBLIC
|
||||
* @param {string|mongoose.Types.ObjectId|null} params.principalId - The ID of the principal (null for PrincipalType.PUBLIC)
|
||||
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
|
||||
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
|
||||
* @param {string} params.accessRoleId - The ID of the role (e.g., AccessRoleIds.AGENT_VIEWER, AccessRoleIds.AGENT_EDITOR)
|
||||
|
|
@ -70,10 +70,21 @@ const grantPermission = async ({
|
|||
}
|
||||
|
||||
if (principalType !== PrincipalType.PUBLIC && !principalId) {
|
||||
throw new Error('Principal ID is required for user and group principals');
|
||||
throw new Error('Principal ID is required for user, group, and role principals');
|
||||
}
|
||||
|
||||
if (principalId && !mongoose.Types.ObjectId.isValid(principalId)) {
|
||||
// Validate principalId based on type
|
||||
if (principalId && principalType === PrincipalType.ROLE) {
|
||||
// Role IDs are strings (role names)
|
||||
if (typeof principalId !== 'string' || principalId.trim().length === 0) {
|
||||
throw new Error(`Invalid role ID: ${principalId}`);
|
||||
}
|
||||
} else if (
|
||||
principalType &&
|
||||
principalType !== PrincipalType.PUBLIC &&
|
||||
!mongoose.Types.ObjectId.isValid(principalId)
|
||||
) {
|
||||
// User and Group IDs must be valid ObjectIds
|
||||
throw new Error(`Invalid principal ID: ${principalId}`);
|
||||
}
|
||||
|
||||
|
|
@ -115,12 +126,13 @@ const grantPermission = async ({
|
|||
* Check if a user has specific permission bits on a resource
|
||||
* @param {Object} params - Parameters for checking permissions
|
||||
* @param {string|mongoose.Types.ObjectId} params.userId - The ID of the user
|
||||
* @param {string} [params.role] - Optional user role (if not provided, will query from DB)
|
||||
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
|
||||
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
|
||||
* @param {number} params.requiredPermissions - The permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT)
|
||||
* @returns {Promise<boolean>} Whether the user has the required permission bits
|
||||
*/
|
||||
const checkPermission = async ({ userId, resourceType, resourceId, requiredPermission }) => {
|
||||
const checkPermission = async ({ userId, role, resourceType, resourceId, requiredPermission }) => {
|
||||
try {
|
||||
if (typeof requiredPermission !== 'number' || requiredPermission < 1) {
|
||||
throw new Error('requiredPermission must be a positive number');
|
||||
|
|
@ -129,7 +141,7 @@ const checkPermission = async ({ userId, resourceType, resourceId, requiredPermi
|
|||
validateResourceType(resourceType);
|
||||
|
||||
// Get all principals for the user (user + groups + public)
|
||||
const principals = await getUserPrincipals(userId);
|
||||
const principals = await getUserPrincipals({ userId, role });
|
||||
|
||||
if (principals.length === 0) {
|
||||
return false;
|
||||
|
|
@ -150,16 +162,17 @@ const checkPermission = async ({ userId, resourceType, resourceId, requiredPermi
|
|||
* Get effective permission bitmask for a user on a resource
|
||||
* @param {Object} params - Parameters for getting effective permissions
|
||||
* @param {string|mongoose.Types.ObjectId} params.userId - The ID of the user
|
||||
* @param {string} [params.role] - Optional user role (if not provided, will query from DB)
|
||||
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
|
||||
* @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource
|
||||
* @returns {Promise<number>} Effective permission bitmask
|
||||
*/
|
||||
const getEffectivePermissions = async ({ userId, resourceType, resourceId }) => {
|
||||
const getEffectivePermissions = async ({ userId, role, resourceType, resourceId }) => {
|
||||
try {
|
||||
validateResourceType(resourceType);
|
||||
|
||||
// Get all principals for the user (user + groups + public)
|
||||
const principals = await getUserPrincipals(userId);
|
||||
const principals = await getUserPrincipals({ userId, role });
|
||||
|
||||
if (principals.length === 0) {
|
||||
return 0;
|
||||
|
|
@ -175,11 +188,12 @@ const getEffectivePermissions = async ({ userId, resourceType, resourceId }) =>
|
|||
* Find all resources of a specific type that a user has access to with specific permission bits
|
||||
* @param {Object} params - Parameters for finding accessible resources
|
||||
* @param {string|mongoose.Types.ObjectId} params.userId - The ID of the user
|
||||
* @param {string} [params.role] - Optional user role (if not provided, will query from DB)
|
||||
* @param {string} params.resourceType - Type of resource (e.g., 'agent')
|
||||
* @param {number} params.requiredPermissions - The minimum permission bits required (e.g., 1 for VIEW, 3 for VIEW+EDIT)
|
||||
* @returns {Promise<Array>} Array of resource IDs
|
||||
*/
|
||||
const findAccessibleResources = async ({ userId, resourceType, requiredPermissions }) => {
|
||||
const findAccessibleResources = async ({ userId, role, resourceType, requiredPermissions }) => {
|
||||
try {
|
||||
if (typeof requiredPermissions !== 'number' || requiredPermissions < 1) {
|
||||
throw new Error('requiredPermissions must be a positive number');
|
||||
|
|
@ -188,7 +202,7 @@ const findAccessibleResources = async ({ userId, resourceType, requiredPermissio
|
|||
validateResourceType(resourceType);
|
||||
|
||||
// Get all principals for the user (user + groups + public)
|
||||
const principalsList = await getUserPrincipals(userId);
|
||||
const principalsList = await getUserPrincipals({ userId, role });
|
||||
|
||||
if (principalsList.length === 0) {
|
||||
return [];
|
||||
|
|
@ -253,7 +267,7 @@ const getAvailableRoles = async ({ resourceType }) => {
|
|||
* Ensures a principal exists in the database based on TPrincipal data
|
||||
* Creates user if it doesn't exist locally (for Entra ID users)
|
||||
* @param {Object} principal - TPrincipal object from frontend
|
||||
* @param {string} principal.type - 'user', 'group', or 'public'
|
||||
* @param {string} principal.type - PrincipalType.USER, PrincipalType.GROUP, or PrincipalType.PUBLIC
|
||||
* @param {string} [principal.id] - Local database ID (null for Entra ID principals not yet synced)
|
||||
* @param {string} principal.name - Display name
|
||||
* @param {string} [principal.email] - Email address
|
||||
|
|
@ -262,7 +276,7 @@ const getAvailableRoles = async ({ resourceType }) => {
|
|||
* @returns {Promise<string|null>} Returns the principalId for database operations, null for public
|
||||
*/
|
||||
const ensurePrincipalExists = async function (principal) {
|
||||
if (principal.type === 'public') {
|
||||
if (principal.type === PrincipalType.PUBLIC) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +284,7 @@ const ensurePrincipalExists = async function (principal) {
|
|||
return principal.id;
|
||||
}
|
||||
|
||||
if (principal.type === 'user' && principal.source === 'entra') {
|
||||
if (principal.type === PrincipalType.USER && principal.source === 'entra') {
|
||||
if (!principal.email || !principal.idOnTheSource) {
|
||||
throw new Error('Entra ID user principals must have email and idOnTheSource');
|
||||
}
|
||||
|
|
@ -303,7 +317,7 @@ const ensurePrincipalExists = async function (principal) {
|
|||
return userId.toString();
|
||||
}
|
||||
|
||||
if (principal.type === 'group') {
|
||||
if (principal.type === PrincipalType.GROUP) {
|
||||
throw new Error('Group principals should be handled by group-specific methods');
|
||||
}
|
||||
|
||||
|
|
@ -315,7 +329,7 @@ const ensurePrincipalExists = async function (principal) {
|
|||
* Creates group if it doesn't exist locally (for Entra ID groups)
|
||||
* For Entra ID groups, always synchronizes member IDs when authentication context is provided
|
||||
* @param {Object} principal - TPrincipal object from frontend
|
||||
* @param {string} principal.type - Must be 'group'
|
||||
* @param {string} principal.type - Must be PrincipalType.GROUP
|
||||
* @param {string} [principal.id] - Local database ID (null for Entra ID principals not yet synced)
|
||||
* @param {string} principal.name - Display name
|
||||
* @param {string} [principal.email] - Email address
|
||||
|
|
@ -328,8 +342,8 @@ const ensurePrincipalExists = async function (principal) {
|
|||
* @returns {Promise<string>} Returns the groupId for database operations
|
||||
*/
|
||||
const ensureGroupPrincipalExists = async function (principal, authContext = null) {
|
||||
if (principal.type !== 'group') {
|
||||
throw new Error(`Invalid principal type: ${principal.type}. Expected 'group'`);
|
||||
if (principal.type !== PrincipalType.GROUP) {
|
||||
throw new Error(`Invalid principal type: ${principal.type}. Expected '${PrincipalType.GROUP}'`);
|
||||
}
|
||||
|
||||
if (principal.source === 'entra') {
|
||||
|
|
@ -612,10 +626,19 @@ const bulkUpdateResourcePermissions = async ({
|
|||
resourceId,
|
||||
};
|
||||
|
||||
if (principal.type !== 'public') {
|
||||
query.principalId = principal.id;
|
||||
if (principal.type !== PrincipalType.PUBLIC) {
|
||||
query.principalId =
|
||||
principal.type === PrincipalType.ROLE
|
||||
? principal.id
|
||||
: new mongoose.Types.ObjectId(principal.id);
|
||||
}
|
||||
|
||||
const principalModelMap = {
|
||||
[PrincipalType.USER]: PrincipalModel.USER,
|
||||
[PrincipalType.GROUP]: PrincipalModel.GROUP,
|
||||
[PrincipalType.ROLE]: PrincipalModel.ROLE,
|
||||
};
|
||||
|
||||
const update = {
|
||||
$set: {
|
||||
permBits: role.permBits,
|
||||
|
|
@ -628,9 +651,11 @@ const bulkUpdateResourcePermissions = async ({
|
|||
resourceType,
|
||||
resourceId,
|
||||
...(principal.type !== PrincipalType.PUBLIC && {
|
||||
principalId: principal.id,
|
||||
principalModel:
|
||||
principal.type === PrincipalType.USER ? PrincipalModel.USER : PrincipalModel.GROUP,
|
||||
principalId:
|
||||
principal.type === PrincipalType.ROLE
|
||||
? principal.id
|
||||
: new mongoose.Types.ObjectId(principal.id),
|
||||
principalModel: principalModelMap[principal.type],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
@ -677,8 +702,11 @@ const bulkUpdateResourcePermissions = async ({
|
|||
resourceId,
|
||||
};
|
||||
|
||||
if (principal.type !== 'public') {
|
||||
query.principalId = principal.id;
|
||||
if (principal.type !== PrincipalType.PUBLIC) {
|
||||
query.principalId =
|
||||
principal.type === PrincipalType.ROLE
|
||||
? principal.id
|
||||
: new mongoose.Types.ObjectId(principal.id);
|
||||
}
|
||||
|
||||
deleteQueries.push(query);
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ describe('PermissionService', () => {
|
|||
const groupId = new mongoose.Types.ObjectId();
|
||||
const resourceId = new mongoose.Types.ObjectId();
|
||||
const grantedById = new mongoose.Types.ObjectId();
|
||||
const roleResourceId = new mongoose.Types.ObjectId();
|
||||
|
||||
describe('grantPermission', () => {
|
||||
test('should grant permission to a user with a role', async () => {
|
||||
|
|
@ -171,7 +172,7 @@ describe('PermissionService', () => {
|
|||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
grantedBy: grantedById,
|
||||
}),
|
||||
).rejects.toThrow('Principal ID is required for user and group principals');
|
||||
).rejects.toThrow('Principal ID is required for user, group, and role principals');
|
||||
});
|
||||
|
||||
test('should throw error for non-existent role', async () => {
|
||||
|
|
@ -1000,6 +1001,230 @@ describe('PermissionService', () => {
|
|||
expect(publicEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
|
||||
});
|
||||
|
||||
test('should grant permission to a role', async () => {
|
||||
const entry = await grantPermission({
|
||||
principalType: PrincipalType.ROLE,
|
||||
principalId: 'admin',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: roleResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry.principalType).toBe(PrincipalType.ROLE);
|
||||
expect(entry.principalId).toBe('admin');
|
||||
expect(entry.principalModel).toBe(PrincipalModel.ROLE);
|
||||
expect(entry.resourceType).toBe(ResourceType.AGENT);
|
||||
expect(entry.resourceId.toString()).toBe(roleResourceId.toString());
|
||||
|
||||
// Get the role to verify the permission bits are correctly set
|
||||
const role = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
|
||||
expect(entry.permBits).toBe(role.permBits);
|
||||
expect(entry.roleId.toString()).toBe(role._id.toString());
|
||||
});
|
||||
|
||||
test('should check permissions for user with role', async () => {
|
||||
// Grant permission to admin role
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.ROLE,
|
||||
principalId: 'admin',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: roleResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Mock getUserPrincipals to return user with admin role
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: userId },
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const hasPermission = await checkPermission({
|
||||
userId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: roleResourceId,
|
||||
requiredPermission: 1, // VIEW
|
||||
});
|
||||
|
||||
expect(hasPermission).toBe(true);
|
||||
|
||||
// Check that user without admin role cannot access
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: userId },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const hasNoPermission = await checkPermission({
|
||||
userId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: roleResourceId,
|
||||
requiredPermission: 1, // VIEW
|
||||
});
|
||||
|
||||
expect(hasNoPermission).toBe(false);
|
||||
});
|
||||
|
||||
test('should optimize permission checks when role is provided', async () => {
|
||||
const testUserId = new mongoose.Types.ObjectId();
|
||||
const testResourceId = new mongoose.Types.ObjectId();
|
||||
|
||||
// Create a user with EDITOR role
|
||||
const User = mongoose.models.User;
|
||||
await User.create({
|
||||
_id: testUserId,
|
||||
email: 'editor@test.com',
|
||||
emailVerified: true,
|
||||
provider: 'local',
|
||||
role: 'EDITOR',
|
||||
});
|
||||
|
||||
// Grant permission to EDITOR role
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.ROLE,
|
||||
principalId: 'EDITOR',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Mock getUserPrincipals to return user with EDITOR role when called
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: testUserId },
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'EDITOR' },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
// Test 1: Check permission with role provided (optimization should be used)
|
||||
const hasPermissionWithRole = await checkPermission({
|
||||
userId: testUserId,
|
||||
role: 'EDITOR',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
requiredPermission: 1, // VIEW
|
||||
});
|
||||
|
||||
expect(hasPermissionWithRole).toBe(true);
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: testUserId, role: 'EDITOR' });
|
||||
|
||||
// Test 2: Check permission without role (should call getUserPrincipals)
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: testUserId },
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'EDITOR' },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const hasPermissionWithoutRole = await checkPermission({
|
||||
userId: testUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
requiredPermission: 1, // VIEW
|
||||
});
|
||||
|
||||
expect(hasPermissionWithoutRole).toBe(true);
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: testUserId, role: undefined });
|
||||
|
||||
// Test 3: Verify getEffectivePermissions also uses the optimization
|
||||
getUserPrincipals.mockClear();
|
||||
|
||||
const effectiveWithRole = await getEffectivePermissions({
|
||||
userId: testUserId,
|
||||
role: 'EDITOR',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
});
|
||||
|
||||
expect(effectiveWithRole).toBe(3); // EDITOR = VIEW + EDIT
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: testUserId, role: 'EDITOR' });
|
||||
|
||||
// Test 4: Verify findAccessibleResources also uses the optimization
|
||||
getUserPrincipals.mockClear();
|
||||
|
||||
const accessibleWithRole = await findAccessibleResources({
|
||||
userId: testUserId,
|
||||
role: 'EDITOR',
|
||||
resourceType: ResourceType.AGENT,
|
||||
requiredPermissions: 1, // VIEW
|
||||
});
|
||||
|
||||
expect(accessibleWithRole.map((id) => id.toString())).toContain(testResourceId.toString());
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: testUserId, role: 'EDITOR' });
|
||||
});
|
||||
|
||||
test('should handle role changes dynamically', async () => {
|
||||
const testUserId = new mongoose.Types.ObjectId();
|
||||
const testResourceId = new mongoose.Types.ObjectId();
|
||||
|
||||
// Grant permission to ADMIN role only
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.ROLE,
|
||||
principalId: 'ADMIN',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Test with ADMIN role - should have access
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: testUserId },
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'ADMIN' },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const hasAdminAccess = await checkPermission({
|
||||
userId: testUserId,
|
||||
role: 'ADMIN',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
requiredPermission: 7, // Full permissions
|
||||
});
|
||||
|
||||
expect(hasAdminAccess).toBe(true);
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: testUserId, role: 'ADMIN' });
|
||||
|
||||
// Test with USER role - should NOT have access
|
||||
getUserPrincipals.mockClear();
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: testUserId },
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'USER' },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const hasUserAccess = await checkPermission({
|
||||
userId: testUserId,
|
||||
role: 'USER',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
requiredPermission: 1, // Even VIEW
|
||||
});
|
||||
|
||||
expect(hasUserAccess).toBe(false);
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: testUserId, role: 'USER' });
|
||||
|
||||
// Test with EDITOR role - should NOT have access
|
||||
getUserPrincipals.mockClear();
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: testUserId },
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'EDITOR' },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const hasEditorAccess = await checkPermission({
|
||||
userId: testUserId,
|
||||
role: 'EDITOR',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
requiredPermission: 1, // VIEW
|
||||
});
|
||||
|
||||
expect(hasEditorAccess).toBe(false);
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: testUserId, role: 'EDITOR' });
|
||||
});
|
||||
|
||||
test('should work with different resource types', async () => {
|
||||
// Test with promptGroup resources
|
||||
const promptGroupResourceId = new mongoose.Types.ObjectId();
|
||||
|
|
@ -1039,4 +1264,344 @@ describe('PermissionService', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('String vs ObjectId Edge Cases', () => {
|
||||
const stringUserId = new mongoose.Types.ObjectId().toString();
|
||||
const objectIdUserId = new mongoose.Types.ObjectId();
|
||||
const stringGroupId = new mongoose.Types.ObjectId().toString();
|
||||
const objectIdGroupId = new mongoose.Types.ObjectId();
|
||||
const testResourceId = new mongoose.Types.ObjectId();
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear any existing ACL entries
|
||||
await AclEntry.deleteMany({});
|
||||
getUserPrincipals.mockReset();
|
||||
});
|
||||
|
||||
test('should handle string userId in grantPermission', async () => {
|
||||
const entry = await grantPermission({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: stringUserId, // Pass string
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry.principalType).toBe(PrincipalType.USER);
|
||||
// Should be stored as ObjectId
|
||||
expect(entry.principalId).toBeInstanceOf(mongoose.Types.ObjectId);
|
||||
expect(entry.principalId.toString()).toBe(stringUserId);
|
||||
});
|
||||
|
||||
test('should handle string groupId in grantPermission', async () => {
|
||||
const entry = await grantPermission({
|
||||
principalType: PrincipalType.GROUP,
|
||||
principalId: stringGroupId, // Pass string
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry.principalType).toBe(PrincipalType.GROUP);
|
||||
// Should be stored as ObjectId
|
||||
expect(entry.principalId).toBeInstanceOf(mongoose.Types.ObjectId);
|
||||
expect(entry.principalId.toString()).toBe(stringGroupId);
|
||||
});
|
||||
|
||||
test('should handle string roleId in grantPermission for ROLE type', async () => {
|
||||
const roleString = 'moderator';
|
||||
|
||||
const entry = await grantPermission({
|
||||
principalType: PrincipalType.ROLE,
|
||||
principalId: roleString,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry.principalType).toBe(PrincipalType.ROLE);
|
||||
// Should remain as string for ROLE type
|
||||
expect(typeof entry.principalId).toBe('string');
|
||||
expect(entry.principalId).toBe(roleString);
|
||||
expect(entry.principalModel).toBe(PrincipalModel.ROLE);
|
||||
});
|
||||
|
||||
test('should check permissions correctly when permission granted with string userId', async () => {
|
||||
// Grant permission with string userId
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: stringUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Mock getUserPrincipals to return ObjectId (as it should after our fix)
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: new mongoose.Types.ObjectId(stringUserId),
|
||||
},
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
// Check permission with string userId
|
||||
const hasPermission = await checkPermission({
|
||||
userId: stringUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
requiredPermission: 1, // VIEW
|
||||
});
|
||||
|
||||
expect(hasPermission).toBe(true);
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: stringUserId, role: undefined });
|
||||
});
|
||||
|
||||
test('should check permissions correctly when permission granted with ObjectId', async () => {
|
||||
// Grant permission with ObjectId
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: objectIdUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Mock getUserPrincipals to return ObjectId
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{ principalType: PrincipalType.USER, principalId: objectIdUserId },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
// Check permission with ObjectId
|
||||
const hasPermission = await checkPermission({
|
||||
userId: objectIdUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
requiredPermission: 7, // Full permissions
|
||||
});
|
||||
|
||||
expect(hasPermission).toBe(true);
|
||||
expect(getUserPrincipals).toHaveBeenCalledWith({ userId: objectIdUserId, role: undefined });
|
||||
});
|
||||
|
||||
test('should handle bulkUpdateResourcePermissions with string IDs', async () => {
|
||||
const updatedPrincipals = [
|
||||
{
|
||||
type: PrincipalType.USER,
|
||||
id: stringUserId, // String ID
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
},
|
||||
{
|
||||
type: PrincipalType.GROUP,
|
||||
id: stringGroupId, // String ID
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
},
|
||||
{
|
||||
type: PrincipalType.ROLE,
|
||||
id: 'admin', // String ID (should remain string)
|
||||
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
||||
},
|
||||
];
|
||||
|
||||
const results = await bulkUpdateResourcePermissions({
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
updatedPrincipals,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
expect(results.granted).toHaveLength(3);
|
||||
expect(results.errors).toHaveLength(0);
|
||||
|
||||
// Verify USER entry has ObjectId
|
||||
const userEntry = await AclEntry.findOne({
|
||||
principalType: PrincipalType.USER,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
});
|
||||
expect(userEntry.principalId).toBeInstanceOf(mongoose.Types.ObjectId);
|
||||
expect(userEntry.principalId.toString()).toBe(stringUserId);
|
||||
|
||||
// Verify GROUP entry has ObjectId
|
||||
const groupEntry = await AclEntry.findOne({
|
||||
principalType: PrincipalType.GROUP,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
});
|
||||
expect(groupEntry.principalId).toBeInstanceOf(mongoose.Types.ObjectId);
|
||||
expect(groupEntry.principalId.toString()).toBe(stringGroupId);
|
||||
|
||||
// Verify ROLE entry has string
|
||||
const roleEntry = await AclEntry.findOne({
|
||||
principalType: PrincipalType.ROLE,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
});
|
||||
expect(typeof roleEntry.principalId).toBe('string');
|
||||
expect(roleEntry.principalId).toBe('admin');
|
||||
});
|
||||
|
||||
test('should handle revoking permissions with string IDs in bulkUpdateResourcePermissions', async () => {
|
||||
// First grant permissions with ObjectIds
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: objectIdUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.GROUP,
|
||||
principalId: objectIdGroupId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Revoke using string IDs
|
||||
const revokedPrincipals = [
|
||||
{
|
||||
type: PrincipalType.USER,
|
||||
id: objectIdUserId.toString(), // String version of ObjectId
|
||||
},
|
||||
{
|
||||
type: PrincipalType.GROUP,
|
||||
id: objectIdGroupId.toString(), // String version of ObjectId
|
||||
},
|
||||
];
|
||||
|
||||
const results = await bulkUpdateResourcePermissions({
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
revokedPrincipals,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
expect(results.revoked).toHaveLength(2);
|
||||
expect(results.errors).toHaveLength(0);
|
||||
|
||||
// Verify permissions were actually revoked
|
||||
const remainingEntries = await AclEntry.find({
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
});
|
||||
expect(remainingEntries).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should find accessible resources when permissions granted with mixed ID types', async () => {
|
||||
const resource1 = new mongoose.Types.ObjectId();
|
||||
const resource2 = new mongoose.Types.ObjectId();
|
||||
const resource3 = new mongoose.Types.ObjectId();
|
||||
|
||||
// Grant with string userId
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: stringUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: resource1,
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Grant with ObjectId userId (same user)
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: new mongoose.Types.ObjectId(stringUserId),
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: resource2,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Grant to role
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.ROLE,
|
||||
principalId: 'admin',
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: resource3,
|
||||
accessRoleId: AccessRoleIds.AGENT_OWNER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Mock getUserPrincipals to return user with admin role
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: new mongoose.Types.ObjectId(stringUserId),
|
||||
},
|
||||
{ principalType: PrincipalType.ROLE, principalId: 'admin' },
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const accessibleResources = await findAccessibleResources({
|
||||
userId: stringUserId,
|
||||
role: 'admin',
|
||||
resourceType: ResourceType.AGENT,
|
||||
requiredPermissions: 1, // VIEW
|
||||
});
|
||||
|
||||
// Should find all three resources
|
||||
expect(accessibleResources).toHaveLength(3);
|
||||
const resourceIds = accessibleResources.map((id) => id.toString());
|
||||
expect(resourceIds).toContain(resource1.toString());
|
||||
expect(resourceIds).toContain(resource2.toString());
|
||||
expect(resourceIds).toContain(resource3.toString());
|
||||
});
|
||||
|
||||
test('should get effective permissions with mixed ID types', async () => {
|
||||
// Grant VIEW permission with string userId
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: stringUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_VIEWER,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Grant EDIT permission to a group with string groupId
|
||||
await grantPermission({
|
||||
principalType: PrincipalType.GROUP,
|
||||
principalId: stringGroupId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
accessRoleId: AccessRoleIds.AGENT_EDITOR,
|
||||
grantedBy: grantedById,
|
||||
});
|
||||
|
||||
// Mock getUserPrincipals to return ObjectIds (as it should after our fix)
|
||||
getUserPrincipals.mockResolvedValue([
|
||||
{
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: new mongoose.Types.ObjectId(stringUserId),
|
||||
},
|
||||
{
|
||||
principalType: PrincipalType.GROUP,
|
||||
principalId: new mongoose.Types.ObjectId(stringGroupId),
|
||||
},
|
||||
{ principalType: PrincipalType.PUBLIC },
|
||||
]);
|
||||
|
||||
const effectivePermissions = await getEffectivePermissions({
|
||||
userId: stringUserId,
|
||||
resourceType: ResourceType.AGENT,
|
||||
resourceId: testResourceId,
|
||||
});
|
||||
|
||||
// Should combine VIEW (1) and EDIT (3) permissions
|
||||
expect(effectivePermissions).toBe(3); // EDITOR includes VIEW
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -57,10 +57,12 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
|
|||
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,
|
||||
},
|
||||
},
|
||||
marketplace: {
|
||||
|
|
@ -88,6 +90,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
|
|||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: loadedInterface.peoplePicker.user?.users,
|
||||
[Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.user?.groups,
|
||||
[Permissions.VIEW_ROLES]: loadedInterface.peoplePicker.user?.roles,
|
||||
},
|
||||
[PermissionTypes.MARKETPLACE]: {
|
||||
[Permissions.USE]: loadedInterface.marketplace.user?.use,
|
||||
|
|
@ -110,6 +113,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
|
|||
[PermissionTypes.PEOPLE_PICKER]: {
|
||||
[Permissions.VIEW_USERS]: loadedInterface.peoplePicker.admin?.users,
|
||||
[Permissions.VIEW_GROUPS]: loadedInterface.peoplePicker.admin?.groups,
|
||||
[Permissions.VIEW_ROLES]: loadedInterface.peoplePicker.admin?.roles,
|
||||
},
|
||||
[PermissionTypes.MARKETPLACE]: {
|
||||
[Permissions.USE]: loadedInterface.marketplace.admin?.use,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue