diff --git a/api/server/controllers/UserController.js b/api/server/controllers/UserController.js index 3702f190db..301c6d2f76 100644 --- a/api/server/controllers/UserController.js +++ b/api/server/controllers/UserController.js @@ -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') diff --git a/api/server/controllers/__tests__/deleteUser.spec.js b/api/server/controllers/__tests__/deleteUser.spec.js index 8dcd217657..a382a6cdc7 100644 --- a/api/server/controllers/__tests__/deleteUser.spec.js +++ b/api/server/controllers/__tests__/deleteUser.spec.js @@ -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(), })); diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index f7b6be612f..fc67b0bc49 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -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} - */ -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, };