parse-dashboard 9.0.0-alpha.7 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -158,7 +158,8 @@ module.exports = function(config, options) {
158
158
  }
159
159
 
160
160
  if (typeof app.masterKey === 'function') {
161
- app.masterKey = await ConfigKeyCache.get(app.appId, 'masterKey', app.masterKeyTtl, app.masterKey);
161
+ const cacheKey = matchingAccess.readOnly ? 'readOnlyMasterKey' : 'masterKey';
162
+ app.masterKey = await ConfigKeyCache.get(app.appId, cacheKey, app.masterKeyTtl, app.masterKey);
162
163
  }
163
164
 
164
165
  return app;
@@ -197,9 +198,11 @@ module.exports = function(config, options) {
197
198
  // In-memory conversation storage (consider using Redis in future)
198
199
  const conversations = new Map();
199
200
 
200
- // Agent API endpoint for handling AI requests - scoped to specific app
201
- app.post('/apps/:appId/agent', async (req, res) => {
201
+ // Agent API endpoint handler
202
+ async function agentHandler(req, res) {
202
203
  try {
204
+ const authentication = req.user;
205
+
203
206
  const { message, modelName, conversationId, permissions } = req.body || {};
204
207
  const { appId } = req.params;
205
208
 
@@ -221,11 +224,40 @@ module.exports = function(config, options) {
221
224
  }
222
225
 
223
226
  // Find the app in the configuration
224
- const app = config.apps.find(app => (app.appNameForURL || app.appName) === appId);
225
- if (!app) {
227
+ const appConfig = config.apps.find(a => (a.appNameForURL || a.appName) === appId);
228
+ if (!appConfig) {
226
229
  return res.status(404).json({ error: `App "${appId}" not found` });
227
230
  }
228
231
 
232
+ // Cross-app access control — restrict to apps the authenticated user has access to
233
+ const appsUserHasAccess = authentication && authentication.appsUserHasAccessTo;
234
+ let isPerAppReadOnly = false;
235
+ if (appsUserHasAccess) {
236
+ const matchingAccess = appsUserHasAccess.find(access => access.appId === appConfig.appId);
237
+ if (!matchingAccess) {
238
+ return res.status(403).json({ error: 'Forbidden: you do not have access to this app' });
239
+ }
240
+ isPerAppReadOnly = !!matchingAccess.readOnly;
241
+ }
242
+
243
+ // Determine if the user is read-only (globally or per-app)
244
+ const isReadOnly = (authentication && authentication.isReadOnly) || isPerAppReadOnly;
245
+
246
+ // Build the app context — always shallow copy to avoid mutating the shared config
247
+ const appContext = { ...appConfig };
248
+ if (isReadOnly) {
249
+ if (!appConfig.readOnlyMasterKey) {
250
+ return res.status(400).json({ error: 'You need to provide a readOnlyMasterKey to use read-only features.' });
251
+ }
252
+ appContext.masterKey = appConfig.readOnlyMasterKey;
253
+ }
254
+
255
+ // Resolve function-typed masterKey (supports dynamic key rotation via ConfigKeyCache)
256
+ if (typeof appContext.masterKey === 'function') {
257
+ const cacheKey = isReadOnly ? 'readOnlyMasterKey' : 'masterKey';
258
+ appContext.masterKey = await ConfigKeyCache.get(appContext.appId, cacheKey, appContext.masterKeyTtl, appContext.masterKey);
259
+ }
260
+
229
261
  // Find the requested model
230
262
  const modelConfig = config.agent.models.find(model => model.name === modelName);
231
263
  if (!modelConfig) {
@@ -258,8 +290,12 @@ module.exports = function(config, options) {
258
290
  // Array to track database operations for this request
259
291
  const operationLog = [];
260
292
 
293
+ // Read-only users: override client permissions to deny all write operations,
294
+ // preventing privilege escalation via self-authorized permissions in the request body
295
+ const effectivePermissions = isReadOnly ? {} : (permissions || {});
296
+
261
297
  // Make request to OpenAI API with app context and conversation history
262
- const response = await makeOpenAIRequest(message, model, apiKey, app, conversationHistory, operationLog, permissions);
298
+ const response = await makeOpenAIRequest(message, model, apiKey, appContext, conversationHistory, operationLog, effectivePermissions);
263
299
 
264
300
  // Update conversation history with user message and AI response
265
301
  conversationHistory.push(
@@ -280,7 +316,7 @@ module.exports = function(config, options) {
280
316
  conversationId: finalConversationId,
281
317
  debug: {
282
318
  timestamp: new Date().toISOString(),
283
- appId: app.appId,
319
+ appId: appContext.appId,
284
320
  modelUsed: model,
285
321
  operations: operationLog
286
322
  }
@@ -291,7 +327,19 @@ module.exports = function(config, options) {
291
327
  const errorMessage = error.message || 'Provider error';
292
328
  res.status(500).json({ error: `Error: ${errorMessage}` });
293
329
  }
294
- });
330
+ }
331
+
332
+ // Agent API endpoint — middleware chain: auth check (401) → CSRF validation (403) → handler
333
+ app.post('/apps/:appId/agent',
334
+ (req, res, next) => {
335
+ if (users && (!req.user || !req.user.isAuthenticated)) {
336
+ return res.status(401).json({ error: 'Unauthorized' });
337
+ }
338
+ next();
339
+ },
340
+ Authentication.csrfProtection,
341
+ agentHandler
342
+ );
295
343
 
296
344
  /**
297
345
  * Database function tools for the AI agent
@@ -1115,7 +1163,7 @@ You have direct access to the Parse database through function calls, so you can
1115
1163
  });
1116
1164
 
1117
1165
  // For every other request, go to index.html. Let client-side handle the rest.
1118
- app.get('{*splat}', function(req, res) {
1166
+ app.get('{*splat}', Authentication.csrfProtection, function(req, res) {
1119
1167
  if (users && (!req.user || !req.user.isAuthenticated)) {
1120
1168
  const redirect = req.url.replace('/login', '');
1121
1169
  if (redirect.length > 1) {
@@ -1139,6 +1187,7 @@ You have direct access to the Parse database through function calls, so you can
1139
1187
  </head>
1140
1188
  <body>
1141
1189
  <div id="browser_mount"></div>
1190
+ <script id="csrf" type="application/json">"${req.csrfToken()}"</script>
1142
1191
  <script src="${mountPath}bundles/dashboard.bundle.js"></script>
1143
1192
  </body>
1144
1193
  </html>