mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-24 16:46:33 +01:00
🧹 chore: Consolidate getSoleOwnedResourceIds into data-schemas and use db object in PermissionService
Move getSoleOwnedResourceIds from PermissionService to data-schemas aclEntry methods, update PermissionService to use the db object pattern instead of destructured imports from ~/models, and update UserController + tests accordingly.
This commit is contained in:
parent
7829fa9eca
commit
87a3b8221a
3 changed files with 31 additions and 105 deletions
|
|
@ -22,7 +22,6 @@ const { getMCPManager, getFlowStateManager, getMCPServersRegistry } = require('~
|
|||
const { invalidateCachedTools } = require('~/server/services/Config/getCachedTools');
|
||||
const { processDeleteRequest } = require('~/server/services/Files/process');
|
||||
const { getAppConfig } = require('~/server/services/Config');
|
||||
const { getSoleOwnedResourceIds } = require('~/server/services/PermissionService');
|
||||
const { getLogStores } = require('~/cache');
|
||||
const db = require('~/models');
|
||||
|
||||
|
|
@ -111,7 +110,7 @@ const deleteUserMcpServers = async (userId) => {
|
|||
}
|
||||
|
||||
const userObjectId = new mongoose.Types.ObjectId(userId);
|
||||
const soleOwnedIds = await getSoleOwnedResourceIds(userObjectId, ResourceType.MCPSERVER);
|
||||
const soleOwnedIds = await db.getSoleOwnedResourceIds(userObjectId, ResourceType.MCPSERVER);
|
||||
|
||||
const authoredServers = await MCPServer.find({ author: userObjectId })
|
||||
.select('_id serverName')
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ jest.mock('~/models', () => ({
|
|||
deleteTokens: jest.fn(),
|
||||
removeUserFromAllGroups: jest.fn(),
|
||||
deleteAclEntries: jest.fn(),
|
||||
getSoleOwnedResourceIds: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/PluginService', () => ({
|
||||
|
|
@ -100,10 +101,6 @@ jest.mock('~/server/services/Config', () => ({
|
|||
getAppConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/server/services/PermissionService', () => ({
|
||||
getSoleOwnedResourceIds: jest.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
jest.mock('~/cache', () => ({
|
||||
getLogStores: jest.fn(),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
const mongoose = require('mongoose');
|
||||
const { isEnabled } = require('@librechat/api');
|
||||
const { getTransactionSupport, logger } = require('@librechat/data-schemas');
|
||||
const {
|
||||
ResourceType,
|
||||
PrincipalType,
|
||||
PrincipalModel,
|
||||
PermissionBits,
|
||||
} = require('librechat-data-provider');
|
||||
const { ResourceType, PrincipalType, PrincipalModel } = require('librechat-data-provider');
|
||||
const {
|
||||
entraIdPrincipalFeatureEnabled,
|
||||
getUserOwnedEntraGroups,
|
||||
|
|
@ -14,28 +9,7 @@ const {
|
|||
getGroupMembers,
|
||||
getGroupOwners,
|
||||
} = require('~/server/services/GraphApiService');
|
||||
const {
|
||||
findAccessibleResources: findAccessibleResourcesACL,
|
||||
getEffectivePermissions: getEffectivePermissionsACL,
|
||||
getEffectivePermissionsForResources: getEffectivePermissionsForResourcesACL,
|
||||
grantPermission: grantPermissionACL,
|
||||
findEntriesByPrincipalsAndResource,
|
||||
findRolesByResourceType,
|
||||
findPublicResourceIds,
|
||||
bulkWriteAclEntries,
|
||||
findGroupByExternalId,
|
||||
findRoleByIdentifier,
|
||||
deleteAclEntries,
|
||||
getUserPrincipals,
|
||||
findGroupByQuery,
|
||||
updateGroupById,
|
||||
bulkUpdateGroups,
|
||||
hasPermission,
|
||||
createGroup,
|
||||
createUser,
|
||||
updateUser,
|
||||
findUser,
|
||||
} = require('~/models');
|
||||
const db = require('~/models');
|
||||
|
||||
/** @type {boolean|null} */
|
||||
let transactionSupportCache = null;
|
||||
|
|
@ -107,7 +81,7 @@ const grantPermission = async ({
|
|||
validateResourceType(resourceType);
|
||||
|
||||
// Get the role to determine permission bits
|
||||
const role = await findRoleByIdentifier(accessRoleId);
|
||||
const role = await db.findRoleByIdentifier(accessRoleId);
|
||||
if (!role) {
|
||||
throw new Error(`Role ${accessRoleId} not found`);
|
||||
}
|
||||
|
|
@ -118,7 +92,7 @@ const grantPermission = async ({
|
|||
`Role ${accessRoleId} is for ${role.resourceType} resources, not ${resourceType}`,
|
||||
);
|
||||
}
|
||||
return await grantPermissionACL(
|
||||
return await db.grantPermission(
|
||||
principalType,
|
||||
principalId,
|
||||
resourceType,
|
||||
|
|
@ -152,13 +126,13 @@ const checkPermission = async ({ userId, role, resourceType, resourceId, require
|
|||
|
||||
validateResourceType(resourceType);
|
||||
|
||||
const principals = await getUserPrincipals({ userId, role });
|
||||
const principals = await db.getUserPrincipals({ userId, role });
|
||||
|
||||
if (principals.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return await hasPermission(principals, resourceType, resourceId, requiredPermission);
|
||||
return await db.hasPermission(principals, resourceType, resourceId, requiredPermission);
|
||||
} catch (error) {
|
||||
logger.error(`[PermissionService.checkPermission] Error: ${error.message}`);
|
||||
if (error.message.includes('requiredPermission must be')) {
|
||||
|
|
@ -181,13 +155,13 @@ const getEffectivePermissions = async ({ userId, role, resourceType, resourceId
|
|||
try {
|
||||
validateResourceType(resourceType);
|
||||
|
||||
const principals = await getUserPrincipals({ userId, role });
|
||||
const principals = await db.getUserPrincipals({ userId, role });
|
||||
|
||||
if (principals.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return await getEffectivePermissionsACL(principals, resourceType, resourceId);
|
||||
return await db.getEffectivePermissions(principals, resourceType, resourceId);
|
||||
} catch (error) {
|
||||
logger.error(`[PermissionService.getEffectivePermissions] Error: ${error.message}`);
|
||||
return 0;
|
||||
|
|
@ -217,10 +191,10 @@ const getResourcePermissionsMap = async ({ userId, role, resourceType, resourceI
|
|||
|
||||
try {
|
||||
// Get user principals (user + groups + public)
|
||||
const principals = await getUserPrincipals({ userId, role });
|
||||
const principals = await db.getUserPrincipals({ userId, role });
|
||||
|
||||
// Use batch method from aclEntry
|
||||
const permissionsMap = await getEffectivePermissionsForResourcesACL(
|
||||
const permissionsMap = await db.getEffectivePermissionsForResources(
|
||||
principals,
|
||||
resourceType,
|
||||
resourceIds,
|
||||
|
|
@ -255,12 +229,12 @@ const findAccessibleResources = async ({ userId, role, resourceType, requiredPer
|
|||
validateResourceType(resourceType);
|
||||
|
||||
// Get all principals for the user (user + groups + public)
|
||||
const principalsList = await getUserPrincipals({ userId, role });
|
||||
const principalsList = await db.getUserPrincipals({ userId, role });
|
||||
|
||||
if (principalsList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await findAccessibleResourcesACL(principalsList, resourceType, requiredPermissions);
|
||||
return await db.findAccessibleResources(principalsList, resourceType, requiredPermissions);
|
||||
} catch (error) {
|
||||
logger.error(`[PermissionService.findAccessibleResources] Error: ${error.message}`);
|
||||
// Re-throw validation errors
|
||||
|
|
@ -286,7 +260,7 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio
|
|||
|
||||
validateResourceType(resourceType);
|
||||
|
||||
return await findPublicResourceIds(resourceType, requiredPermissions);
|
||||
return await db.findPublicResourceIds(resourceType, requiredPermissions);
|
||||
} catch (error) {
|
||||
logger.error(`[PermissionService.findPubliclyAccessibleResources] Error: ${error.message}`);
|
||||
if (error.message.includes('requiredPermissions must be')) {
|
||||
|
|
@ -305,7 +279,7 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio
|
|||
const getAvailableRoles = async ({ resourceType }) => {
|
||||
validateResourceType(resourceType);
|
||||
|
||||
return await findRolesByResourceType(resourceType);
|
||||
return await db.findRolesByResourceType(resourceType);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -334,15 +308,15 @@ const ensurePrincipalExists = async function (principal) {
|
|||
throw new Error('Entra ID user principals must have email and idOnTheSource');
|
||||
}
|
||||
|
||||
let existingUser = await findUser({ idOnTheSource: principal.idOnTheSource });
|
||||
let existingUser = await db.findUser({ idOnTheSource: principal.idOnTheSource });
|
||||
|
||||
if (!existingUser) {
|
||||
existingUser = await findUser({ email: principal.email });
|
||||
existingUser = await db.findUser({ email: principal.email });
|
||||
}
|
||||
|
||||
if (existingUser) {
|
||||
if (!existingUser.idOnTheSource && principal.idOnTheSource) {
|
||||
await updateUser(existingUser._id, {
|
||||
await db.updateUser(existingUser._id, {
|
||||
idOnTheSource: principal.idOnTheSource,
|
||||
provider: 'openid',
|
||||
});
|
||||
|
|
@ -358,7 +332,7 @@ const ensurePrincipalExists = async function (principal) {
|
|||
idOnTheSource: principal.idOnTheSource,
|
||||
};
|
||||
|
||||
const userId = await createUser(userData, true, true);
|
||||
const userId = await db.createUser(userData, true, true);
|
||||
return userId.toString();
|
||||
}
|
||||
|
||||
|
|
@ -423,10 +397,10 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null
|
|||
}
|
||||
}
|
||||
|
||||
let existingGroup = await findGroupByExternalId(principal.idOnTheSource, 'entra');
|
||||
let existingGroup = await db.findGroupByExternalId(principal.idOnTheSource, 'entra');
|
||||
|
||||
if (!existingGroup && principal.email) {
|
||||
existingGroup = await findGroupByQuery({ email: principal.email.toLowerCase() });
|
||||
existingGroup = await db.findGroupByQuery({ email: principal.email.toLowerCase() });
|
||||
}
|
||||
|
||||
if (existingGroup) {
|
||||
|
|
@ -455,7 +429,7 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null
|
|||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
await updateGroupById(existingGroup._id, updateData);
|
||||
await db.updateGroupById(existingGroup._id, updateData);
|
||||
}
|
||||
|
||||
return existingGroup._id.toString();
|
||||
|
|
@ -476,7 +450,7 @@ const ensureGroupPrincipalExists = async function (principal, authContext = null
|
|||
groupData.description = principal.description;
|
||||
}
|
||||
|
||||
const newGroup = await createGroup(groupData);
|
||||
const newGroup = await db.createGroup(groupData);
|
||||
return newGroup._id.toString();
|
||||
}
|
||||
if (principal.id && authContext == null) {
|
||||
|
|
@ -523,7 +497,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null)
|
|||
|
||||
const sessionOptions = session ? { session } : {};
|
||||
|
||||
await bulkUpdateGroups(
|
||||
await db.bulkUpdateGroups(
|
||||
{
|
||||
idOnTheSource: { $in: allGroupIds },
|
||||
source: 'entra',
|
||||
|
|
@ -533,7 +507,7 @@ const syncUserEntraGroupMemberships = async (user, accessToken, session = null)
|
|||
sessionOptions,
|
||||
);
|
||||
|
||||
await bulkUpdateGroups(
|
||||
await db.bulkUpdateGroups(
|
||||
{
|
||||
source: 'entra',
|
||||
memberIds: user.idOnTheSource,
|
||||
|
|
@ -566,7 +540,7 @@ const hasPublicPermission = async ({ resourceType, resourceId, requiredPermissio
|
|||
// Use public principal to check permissions
|
||||
const publicPrincipal = [{ principalType: PrincipalType.PUBLIC }];
|
||||
|
||||
const entries = await findEntriesByPrincipalsAndResource(
|
||||
const entries = await db.findEntriesByPrincipalsAndResource(
|
||||
publicPrincipal,
|
||||
resourceType,
|
||||
resourceId,
|
||||
|
|
@ -631,7 +605,7 @@ const bulkUpdateResourcePermissions = async ({
|
|||
|
||||
const sessionOptions = localSession ? { session: localSession } : {};
|
||||
|
||||
const roles = await findRolesByResourceType(resourceType);
|
||||
const roles = await db.findRolesByResourceType(resourceType);
|
||||
const rolesMap = new Map();
|
||||
roles.forEach((role) => {
|
||||
rolesMap.set(role.accessRoleId, role);
|
||||
|
|
@ -735,7 +709,7 @@ const bulkUpdateResourcePermissions = async ({
|
|||
}
|
||||
|
||||
if (bulkWrites.length > 0) {
|
||||
await bulkWriteAclEntries(bulkWrites, sessionOptions);
|
||||
await db.bulkWriteAclEntries(bulkWrites, sessionOptions);
|
||||
}
|
||||
|
||||
const deleteQueries = [];
|
||||
|
|
@ -776,7 +750,7 @@ const bulkUpdateResourcePermissions = async ({
|
|||
}
|
||||
|
||||
if (deleteQueries.length > 0) {
|
||||
await deleteAclEntries({ $or: deleteQueries }, sessionOptions);
|
||||
await db.deleteAclEntries({ $or: deleteQueries }, sessionOptions);
|
||||
}
|
||||
|
||||
if (shouldEndSession && supportsTransactions) {
|
||||
|
|
@ -805,49 +779,6 @@ const bulkUpdateResourcePermissions = async ({
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns resource IDs where the given user is the sole owner
|
||||
* (no other principal holds the DELETE bit on the same resource).
|
||||
* @param {mongoose.Types.ObjectId} userObjectId
|
||||
* @param {string|string[]} resourceTypes - One or more ResourceType values.
|
||||
* @returns {Promise<mongoose.Types.ObjectId[]>}
|
||||
*/
|
||||
const getSoleOwnedResourceIds = async (userObjectId, resourceTypes) => {
|
||||
const types = Array.isArray(resourceTypes) ? resourceTypes : [resourceTypes];
|
||||
const ownedEntries = await AclEntry.find({
|
||||
principalType: PrincipalType.USER,
|
||||
principalId: userObjectId,
|
||||
resourceType: { $in: types },
|
||||
permBits: { $bitsAllSet: PermissionBits.DELETE },
|
||||
})
|
||||
.select('resourceId')
|
||||
.lean();
|
||||
|
||||
if (ownedEntries.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ownedIds = ownedEntries.map((e) => e.resourceId);
|
||||
|
||||
const otherOwners = await AclEntry.aggregate([
|
||||
{
|
||||
$match: {
|
||||
resourceType: { $in: types },
|
||||
resourceId: { $in: ownedIds },
|
||||
permBits: { $bitsAllSet: PermissionBits.DELETE },
|
||||
$or: [
|
||||
{ principalId: { $ne: userObjectId } },
|
||||
{ principalType: { $ne: PrincipalType.USER } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ $group: { _id: '$resourceId' } },
|
||||
]);
|
||||
|
||||
const multiOwnerIds = new Set(otherOwners.map((doc) => doc._id.toString()));
|
||||
return ownedIds.filter((id) => !multiOwnerIds.has(id.toString()));
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all permissions for a resource (cleanup when resource is deleted)
|
||||
* @param {Object} params - Parameters for removing all permissions
|
||||
|
|
@ -863,7 +794,7 @@ const removeAllPermissions = async ({ resourceType, resourceId }) => {
|
|||
throw new Error(`Invalid resource ID: ${resourceId}`);
|
||||
}
|
||||
|
||||
const result = await deleteAclEntries({
|
||||
const result = await db.deleteAclEntries({
|
||||
resourceType,
|
||||
resourceId,
|
||||
});
|
||||
|
|
@ -888,6 +819,5 @@ module.exports = {
|
|||
ensurePrincipalExists,
|
||||
ensureGroupPrincipalExists,
|
||||
syncUserEntraGroupMemberships,
|
||||
getSoleOwnedResourceIds,
|
||||
removeAllPermissions,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue