WIP: Role as Permission Principal Type

This commit is contained in:
Danny Avila 2025-08-03 19:24:40 -04:00
parent 7c35d17e3d
commit 89f0a4e02f
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
11 changed files with 1167 additions and 38 deletions

View file

@ -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,
@ -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}`);
}
@ -616,6 +627,12 @@ const bulkUpdateResourcePermissions = async ({
query.principalId = principal.id;
}
const principalModelMap = {
[PrincipalType.USER]: PrincipalModel.USER,
[PrincipalType.GROUP]: PrincipalModel.GROUP,
[PrincipalType.ROLE]: PrincipalModel.ROLE,
};
const update = {
$set: {
permBits: role.permBits,
@ -629,8 +646,7 @@ const bulkUpdateResourcePermissions = async ({
resourceId,
...(principal.type !== PrincipalType.PUBLIC && {
principalId: principal.id,
principalModel:
principal.type === PrincipalType.USER ? PrincipalModel.USER : PrincipalModel.GROUP,
principalModel: principalModelMap[principal.type],
}),
},
};

View file

@ -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,72 @@ 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 work with different resource types', async () => {
// Test with promptGroup resources
const promptGroupResourceId = new mongoose.Types.ObjectId();