@@ -158,7 +158,8 @@ module.exports = function(config, options) {
158158 }
159159
160160 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 ) ;
162163 }
163164
164165 return app ;
@@ -197,9 +198,11 @@ module.exports = function(config, options) {
197198 // In-memory conversation storage (consider using Redis in future)
198199 const conversations = new Map ( ) ;
199200
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 ) {
202203 try {
204+ const authentication = req . user ;
205+
203206 const { message, modelName, conversationId, permissions } = req . body || { } ;
204207 const { appId } = req . params ;
205208
@@ -221,11 +224,40 @@ module.exports = function(config, options) {
221224 }
222225
223226 // 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 ) {
226229 return res . status ( 404 ) . json ( { error : `App "${ appId } " not found` } ) ;
227230 }
228231
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+
229261 // Find the requested model
230262 const modelConfig = config . agent . models . find ( model => model . name === modelName ) ;
231263 if ( ! modelConfig ) {
@@ -258,8 +290,12 @@ module.exports = function(config, options) {
258290 // Array to track database operations for this request
259291 const operationLog = [ ] ;
260292
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+
261297 // 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 ) ;
263299
264300 // Update conversation history with user message and AI response
265301 conversationHistory . push (
@@ -280,7 +316,7 @@ module.exports = function(config, options) {
280316 conversationId : finalConversationId ,
281317 debug : {
282318 timestamp : new Date ( ) . toISOString ( ) ,
283- appId : app . appId ,
319+ appId : appContext . appId ,
284320 modelUsed : model ,
285321 operations : operationLog
286322 }
@@ -291,7 +327,19 @@ module.exports = function(config, options) {
291327 const errorMessage = error . message || 'Provider error' ;
292328 res . status ( 500 ) . json ( { error : `Error: ${ errorMessage } ` } ) ;
293329 }
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+ ) ;
295343
296344 /**
297345 * 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
11151163 } ) ;
11161164
11171165 // 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 ) {
11191167 if ( users && ( ! req . user || ! req . user . isAuthenticated ) ) {
11201168 const redirect = req . url . replace ( '/login' , '' ) ;
11211169 if ( redirect . length > 1 ) {
@@ -1139,6 +1187,7 @@ You have direct access to the Parse database through function calls, so you can
11391187 </head>
11401188 <body>
11411189 <div id="browser_mount"></div>
1190+ <script id="csrf" type="application/json">"${ req . csrfToken ( ) } "</script>
11421191 <script src="${ mountPath } bundles/dashboard.bundle.js"></script>
11431192 </body>
11441193 </html>
0 commit comments