🧹 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:
Danny Avila 2026-03-21 13:59:00 -04:00
parent 7829fa9eca
commit 87a3b8221a
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
3 changed files with 31 additions and 105 deletions

View file

@ -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')

View file

@ -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(),
}));

View file

@ -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,
};