2025-08-02 16:02:56 -04:00
|
|
|
import { PrincipalType } from 'librechat-data-provider';
|
2025-06-23 10:22:27 -04:00
|
|
|
import type { Model, Types, DeleteResult, ClientSession } from 'mongoose';
|
|
|
|
import type { IAclEntry } from '~/types';
|
|
|
|
|
|
|
|
export function createAclEntryMethods(mongoose: typeof import('mongoose')) {
|
|
|
|
/**
|
|
|
|
* Find ACL entries for a specific principal (user or group)
|
|
|
|
* @param principalType - The type of principal ('user', 'group')
|
|
|
|
* @param principalId - The ID of the principal
|
|
|
|
* @param resourceType - Optional filter by resource type
|
|
|
|
* @returns Array of ACL entries
|
|
|
|
*/
|
|
|
|
async function findEntriesByPrincipal(
|
|
|
|
principalType: string,
|
|
|
|
principalId: string | Types.ObjectId,
|
|
|
|
resourceType?: string,
|
|
|
|
): Promise<IAclEntry[]> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
const query: Record<string, unknown> = { principalType, principalId };
|
|
|
|
if (resourceType) {
|
|
|
|
query.resourceType = resourceType;
|
|
|
|
}
|
|
|
|
return await AclEntry.find(query).lean();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find ACL entries for a specific resource
|
|
|
|
* @param resourceType - The type of resource ('agent', 'project', 'file')
|
|
|
|
* @param resourceId - The ID of the resource
|
|
|
|
* @returns Array of ACL entries
|
|
|
|
*/
|
|
|
|
async function findEntriesByResource(
|
|
|
|
resourceType: string,
|
|
|
|
resourceId: string | Types.ObjectId,
|
|
|
|
): Promise<IAclEntry[]> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
return await AclEntry.find({ resourceType, resourceId }).lean();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find all ACL entries for a set of principals (including public)
|
|
|
|
* @param principalsList - List of principals, each containing { principalType, principalId }
|
|
|
|
* @param resourceType - The type of resource
|
|
|
|
* @param resourceId - The ID of the resource
|
|
|
|
* @returns Array of matching ACL entries
|
|
|
|
*/
|
|
|
|
async function findEntriesByPrincipalsAndResource(
|
|
|
|
principalsList: Array<{ principalType: string; principalId?: string | Types.ObjectId }>,
|
|
|
|
resourceType: string,
|
|
|
|
resourceId: string | Types.ObjectId,
|
|
|
|
): Promise<IAclEntry[]> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
const principalsQuery = principalsList.map((p) => ({
|
|
|
|
principalType: p.principalType,
|
2025-08-02 16:02:56 -04:00
|
|
|
...(p.principalType !== PrincipalType.PUBLIC && { principalId: p.principalId }),
|
2025-06-23 10:22:27 -04:00
|
|
|
}));
|
|
|
|
|
|
|
|
return await AclEntry.find({
|
|
|
|
$or: principalsQuery,
|
|
|
|
resourceType,
|
|
|
|
resourceId,
|
|
|
|
}).lean();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a set of principals has a specific permission on a resource
|
|
|
|
* @param principalsList - List of principals, each containing { principalType, principalId }
|
|
|
|
* @param resourceType - The type of resource
|
|
|
|
* @param resourceId - The ID of the resource
|
|
|
|
* @param permissionBit - The permission bit to check (use PermissionBits enum)
|
|
|
|
* @returns Whether any of the principals has the permission
|
|
|
|
*/
|
|
|
|
async function hasPermission(
|
|
|
|
principalsList: Array<{ principalType: string; principalId?: string | Types.ObjectId }>,
|
|
|
|
resourceType: string,
|
|
|
|
resourceId: string | Types.ObjectId,
|
|
|
|
permissionBit: number,
|
|
|
|
): Promise<boolean> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
const principalsQuery = principalsList.map((p) => ({
|
|
|
|
principalType: p.principalType,
|
2025-08-02 16:02:56 -04:00
|
|
|
...(p.principalType !== PrincipalType.PUBLIC && { principalId: p.principalId }),
|
2025-06-23 10:22:27 -04:00
|
|
|
}));
|
|
|
|
|
|
|
|
const entry = await AclEntry.findOne({
|
|
|
|
$or: principalsQuery,
|
|
|
|
resourceType,
|
|
|
|
resourceId,
|
|
|
|
permBits: { $bitsAllSet: permissionBit },
|
|
|
|
}).lean();
|
|
|
|
|
|
|
|
return !!entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the combined effective permissions for a set of principals on a resource
|
|
|
|
* @param principalsList - List of principals, each containing { principalType, principalId }
|
|
|
|
* @param resourceType - The type of resource
|
|
|
|
* @param resourceId - The ID of the resource
|
|
|
|
* @returns {Promise<number>} Effective permission bitmask
|
|
|
|
*/
|
|
|
|
async function getEffectivePermissions(
|
|
|
|
principalsList: Array<{ principalType: string; principalId?: string | Types.ObjectId }>,
|
|
|
|
resourceType: string,
|
|
|
|
resourceId: string | Types.ObjectId,
|
|
|
|
): Promise<number> {
|
|
|
|
const aclEntries = await findEntriesByPrincipalsAndResource(
|
|
|
|
principalsList,
|
|
|
|
resourceType,
|
|
|
|
resourceId,
|
|
|
|
);
|
|
|
|
|
|
|
|
let effectiveBits = 0;
|
|
|
|
for (const entry of aclEntries) {
|
|
|
|
effectiveBits |= entry.permBits;
|
|
|
|
}
|
|
|
|
return effectiveBits;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Grant permission to a principal for a resource
|
|
|
|
* @param principalType - The type of principal ('user', 'group', 'public')
|
|
|
|
* @param principalId - The ID of the principal (null for 'public')
|
|
|
|
* @param resourceType - The type of resource
|
|
|
|
* @param resourceId - The ID of the resource
|
|
|
|
* @param permBits - The permission bits to grant
|
|
|
|
* @param grantedBy - The ID of the user granting the permission
|
|
|
|
* @param session - Optional MongoDB session for transactions
|
|
|
|
* @param roleId - Optional role ID to associate with this permission
|
|
|
|
* @returns The created or updated ACL entry
|
|
|
|
*/
|
|
|
|
async function grantPermission(
|
|
|
|
principalType: string,
|
|
|
|
principalId: string | Types.ObjectId | null,
|
|
|
|
resourceType: string,
|
|
|
|
resourceId: string | Types.ObjectId,
|
|
|
|
permBits: number,
|
|
|
|
grantedBy: string | Types.ObjectId,
|
|
|
|
session?: ClientSession,
|
|
|
|
roleId?: string | Types.ObjectId,
|
|
|
|
): Promise<IAclEntry | null> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
const query: Record<string, unknown> = {
|
|
|
|
principalType,
|
|
|
|
resourceType,
|
|
|
|
resourceId,
|
|
|
|
};
|
|
|
|
|
2025-08-02 16:02:56 -04:00
|
|
|
if (principalType !== PrincipalType.PUBLIC) {
|
2025-06-23 10:22:27 -04:00
|
|
|
query.principalId = principalId;
|
2025-08-02 16:02:56 -04:00
|
|
|
query.principalModel = principalType === PrincipalType.USER ? 'User' : 'Group';
|
2025-06-23 10:22:27 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
const update = {
|
|
|
|
$set: {
|
|
|
|
permBits,
|
|
|
|
grantedBy,
|
|
|
|
grantedAt: new Date(),
|
|
|
|
...(roleId && { roleId }),
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
upsert: true,
|
|
|
|
new: true,
|
|
|
|
...(session ? { session } : {}),
|
|
|
|
};
|
|
|
|
|
|
|
|
return await AclEntry.findOneAndUpdate(query, update, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Revoke permissions from a principal for a resource
|
|
|
|
* @param principalType - The type of principal ('user', 'group', 'public')
|
|
|
|
* @param principalId - The ID of the principal (null for 'public')
|
|
|
|
* @param resourceType - The type of resource
|
|
|
|
* @param resourceId - The ID of the resource
|
|
|
|
* @param session - Optional MongoDB session for transactions
|
|
|
|
* @returns The result of the delete operation
|
|
|
|
*/
|
|
|
|
async function revokePermission(
|
|
|
|
principalType: string,
|
|
|
|
principalId: string | Types.ObjectId | null,
|
|
|
|
resourceType: string,
|
|
|
|
resourceId: string | Types.ObjectId,
|
|
|
|
session?: ClientSession,
|
|
|
|
): Promise<DeleteResult> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
const query: Record<string, unknown> = {
|
|
|
|
principalType,
|
|
|
|
resourceType,
|
|
|
|
resourceId,
|
|
|
|
};
|
|
|
|
|
2025-08-02 16:02:56 -04:00
|
|
|
if (principalType !== PrincipalType.PUBLIC) {
|
2025-06-23 10:22:27 -04:00
|
|
|
query.principalId = principalId;
|
|
|
|
}
|
|
|
|
|
|
|
|
const options = session ? { session } : {};
|
|
|
|
|
|
|
|
return await AclEntry.deleteOne(query, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Modify existing permission bits for a principal on a resource
|
|
|
|
* @param principalType - The type of principal ('user', 'group', 'public')
|
|
|
|
* @param principalId - The ID of the principal (null for 'public')
|
|
|
|
* @param resourceType - The type of resource
|
|
|
|
* @param resourceId - The ID of the resource
|
|
|
|
* @param addBits - Permission bits to add
|
|
|
|
* @param removeBits - Permission bits to remove
|
|
|
|
* @param session - Optional MongoDB session for transactions
|
|
|
|
* @returns The updated ACL entry
|
|
|
|
*/
|
|
|
|
async function modifyPermissionBits(
|
|
|
|
principalType: string,
|
|
|
|
principalId: string | Types.ObjectId | null,
|
|
|
|
resourceType: string,
|
|
|
|
resourceId: string | Types.ObjectId,
|
|
|
|
addBits?: number | null,
|
|
|
|
removeBits?: number | null,
|
|
|
|
session?: ClientSession,
|
|
|
|
): Promise<IAclEntry | null> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
const query: Record<string, unknown> = {
|
|
|
|
principalType,
|
|
|
|
resourceType,
|
|
|
|
resourceId,
|
|
|
|
};
|
|
|
|
|
2025-08-02 16:02:56 -04:00
|
|
|
if (principalType !== PrincipalType.PUBLIC) {
|
2025-06-23 10:22:27 -04:00
|
|
|
query.principalId = principalId;
|
|
|
|
}
|
|
|
|
|
|
|
|
const update: Record<string, unknown> = {};
|
|
|
|
|
|
|
|
if (addBits) {
|
|
|
|
update.$bit = { permBits: { or: addBits } };
|
|
|
|
}
|
|
|
|
|
|
|
|
if (removeBits) {
|
|
|
|
if (!update.$bit) update.$bit = {};
|
|
|
|
const bitUpdate = update.$bit as Record<string, unknown>;
|
|
|
|
bitUpdate.permBits = { ...(bitUpdate.permBits as Record<string, unknown>), and: ~removeBits };
|
|
|
|
}
|
|
|
|
|
|
|
|
const options = {
|
|
|
|
new: true,
|
|
|
|
...(session ? { session } : {}),
|
|
|
|
};
|
|
|
|
|
|
|
|
return await AclEntry.findOneAndUpdate(query, update, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find all resources of a specific type that a set of principals has access to
|
|
|
|
* @param principalsList - List of principals, each containing { principalType, principalId }
|
|
|
|
* @param resourceType - The type of resource
|
|
|
|
* @param requiredPermBit - Required permission bit (use PermissionBits enum)
|
|
|
|
* @returns Array of resource IDs
|
|
|
|
*/
|
|
|
|
async function findAccessibleResources(
|
|
|
|
principalsList: Array<{ principalType: string; principalId?: string | Types.ObjectId }>,
|
|
|
|
resourceType: string,
|
|
|
|
requiredPermBit: number,
|
|
|
|
): Promise<Types.ObjectId[]> {
|
|
|
|
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
|
|
|
|
const principalsQuery = principalsList.map((p) => ({
|
|
|
|
principalType: p.principalType,
|
2025-08-02 16:02:56 -04:00
|
|
|
...(p.principalType !== PrincipalType.PUBLIC && { principalId: p.principalId }),
|
2025-06-23 10:22:27 -04:00
|
|
|
}));
|
|
|
|
|
|
|
|
const entries = await AclEntry.find({
|
|
|
|
$or: principalsQuery,
|
|
|
|
resourceType,
|
|
|
|
permBits: { $bitsAllSet: requiredPermBit },
|
|
|
|
}).distinct('resourceId');
|
|
|
|
|
|
|
|
return entries;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
findEntriesByPrincipal,
|
|
|
|
findEntriesByResource,
|
|
|
|
findEntriesByPrincipalsAndResource,
|
|
|
|
hasPermission,
|
|
|
|
getEffectivePermissions,
|
|
|
|
grantPermission,
|
|
|
|
revokePermission,
|
|
|
|
modifyPermissionBits,
|
|
|
|
findAccessibleResources,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export type AclEntryMethods = ReturnType<typeof createAclEntryMethods>;
|