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 { const AclEntry = mongoose.models.AclEntry as Model; const query: Record = { 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 { const AclEntry = mongoose.models.AclEntry as Model; 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 { const AclEntry = mongoose.models.AclEntry as Model; const principalsQuery = principalsList.map((p) => ({ principalType: p.principalType, ...(p.principalType !== 'public' && { principalId: p.principalId }), })); 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 { const AclEntry = mongoose.models.AclEntry as Model; const principalsQuery = principalsList.map((p) => ({ principalType: p.principalType, ...(p.principalType !== 'public' && { principalId: p.principalId }), })); 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} Effective permission bitmask */ async function getEffectivePermissions( principalsList: Array<{ principalType: string; principalId?: string | Types.ObjectId }>, resourceType: string, resourceId: string | Types.ObjectId, ): Promise { 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 { const AclEntry = mongoose.models.AclEntry as Model; const query: Record = { principalType, resourceType, resourceId, }; if (principalType !== 'public') { query.principalId = principalId; query.principalModel = principalType === 'user' ? 'User' : 'Group'; } 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 { const AclEntry = mongoose.models.AclEntry as Model; const query: Record = { principalType, resourceType, resourceId, }; if (principalType !== 'public') { 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 { const AclEntry = mongoose.models.AclEntry as Model; const query: Record = { principalType, resourceType, resourceId, }; if (principalType !== 'public') { query.principalId = principalId; } const update: Record = {}; if (addBits) { update.$bit = { permBits: { or: addBits } }; } if (removeBits) { if (!update.$bit) update.$bit = {}; const bitUpdate = update.$bit as Record; bitUpdate.permBits = { ...(bitUpdate.permBits as Record), 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 { const AclEntry = mongoose.models.AclEntry as Model; const principalsQuery = principalsList.map((p) => ({ principalType: p.principalType, ...(p.principalType !== 'public' && { principalId: p.principalId }), })); 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;