File size: 5,449 Bytes
f0743f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
const { logger } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
const { checkPermission } = require('~/server/services/PermissionService');

/**
 * Generic base middleware factory that creates middleware to check resource access permissions.
 * This middleware expects MongoDB ObjectIds as resource identifiers for ACL permission checks.
 *
 * @param {Object} options - Configuration options
 * @param {string} options.resourceType - The type of resource (e.g., 'agent', 'file', 'project')
 * @param {number} options.requiredPermission - The permission bit required (1=view, 2=edit, 4=delete, 8=share)
 * @param {string} [options.resourceIdParam='resourceId'] - The name of the route parameter containing the resource ID
 * @param {Function} [options.idResolver] - Optional function to resolve custom IDs to ObjectIds
 * @returns {Function} Express middleware function
 *
 * @example
 * // Direct usage with ObjectId (for resources that use MongoDB ObjectId in routes)
 * router.get('/prompts/:promptId',
 *   canAccessResource({ resourceType: 'prompt', requiredPermission: 1 }),
 *   getPrompt
 * );
 *
 * @example
 * // Usage with custom ID resolver (for resources that use custom string IDs)
 * router.get('/agents/:id',
 *   canAccessResource({
 *     resourceType: 'agent',
 *     requiredPermission: 1,
 *     resourceIdParam: 'id',
 *     idResolver: (customId) => resolveAgentId(customId)
 *   }),
 *   getAgent
 * );
 */
const canAccessResource = (options) => {
  const {
    resourceType,
    requiredPermission,
    resourceIdParam = 'resourceId',
    idResolver = null,
  } = options;

  if (!resourceType || typeof resourceType !== 'string') {
    throw new Error('canAccessResource: resourceType is required and must be a string');
  }

  if (!requiredPermission || typeof requiredPermission !== 'number') {
    throw new Error('canAccessResource: requiredPermission is required and must be a number');
  }

  return async (req, res, next) => {
    try {
      // Extract resource ID from route parameters
      const rawResourceId = req.params[resourceIdParam];

      if (!rawResourceId) {
        logger.warn(`[canAccessResource] Missing ${resourceIdParam} in route parameters`);
        return res.status(400).json({
          error: 'Bad Request',
          message: `${resourceIdParam} is required`,
        });
      }

      // Check if user is authenticated
      if (!req.user || !req.user.id) {
        logger.warn(
          `[canAccessResource] Unauthenticated request for ${resourceType} ${rawResourceId}`,
        );
        return res.status(401).json({
          error: 'Unauthorized',
          message: 'Authentication required',
        });
      }
      // if system admin let through
      if (req.user.role === SystemRoles.ADMIN) {
        return next();
      }
      const userId = req.user.id;
      let resourceId = rawResourceId;
      let resourceInfo = null;

      // Resolve custom ID to ObjectId if resolver is provided
      if (idResolver) {
        logger.debug(
          `[canAccessResource] Resolving ${resourceType} custom ID ${rawResourceId} to ObjectId`,
        );

        const resolutionResult = await idResolver(rawResourceId);

        if (!resolutionResult) {
          logger.warn(`[canAccessResource] ${resourceType} not found: ${rawResourceId}`);
          return res.status(404).json({
            error: 'Not Found',
            message: `${resourceType} not found`,
          });
        }

        // Handle different resolver return formats
        if (typeof resolutionResult === 'string' || resolutionResult._id) {
          resourceId = resolutionResult._id || resolutionResult;
          resourceInfo = typeof resolutionResult === 'object' ? resolutionResult : null;
        } else {
          resourceId = resolutionResult;
        }

        logger.debug(
          `[canAccessResource] Resolved ${resourceType} ${rawResourceId} to ObjectId ${resourceId}`,
        );
      }

      // Check permissions using PermissionService with ObjectId
      const hasPermission = await checkPermission({
        userId,
        role: req.user.role,
        resourceType,
        resourceId,
        requiredPermission,
      });

      if (hasPermission) {
        logger.debug(
          `[canAccessResource] User ${userId} has permission ${requiredPermission} on ${resourceType} ${rawResourceId} (${resourceId})`,
        );

        req.resourceAccess = {
          resourceType,
          resourceId, // MongoDB ObjectId for ACL operations
          customResourceId: rawResourceId, // Original ID from route params
          permission: requiredPermission,
          userId,
          ...(resourceInfo && { resourceInfo }),
        };

        return next();
      }

      logger.warn(
        `[canAccessResource] User ${userId} denied access to ${resourceType} ${rawResourceId} ` +
          `(required permission: ${requiredPermission})`,
      );

      return res.status(403).json({
        error: 'Forbidden',
        message: `Insufficient permissions to access this ${resourceType}`,
      });
    } catch (error) {
      logger.error(`[canAccessResource] Error checking access for ${resourceType}:`, error);
      return res.status(500).json({
        error: 'Internal Server Error',
        message: 'Failed to check resource access permissions',
      });
    }
  };
};

module.exports = {
  canAccessResource,
};