mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-23 20:00:15 +01:00
WIP: Role as Permission Principal Type
This commit is contained in:
parent
7c35d17e3d
commit
89f0a4e02f
11 changed files with 1167 additions and 38 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,7 +378,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];
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue