{"record":{"id":"e888c8b8cf72a74a","repo":"actualbudget/actual","slug":"file-access-denied","errorCode":"file-access-denied","errorMessage":"You don't have permissions over this file","messagePattern":"You don't have permissions over this file","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"packages/sync-server/src/app-pluggyai/app-pluggyai.js","lineNumber":40,"sourceCode":"  return isAdmin(userId) || UserService.countUserAccess(fileId, userId) > 0;\n}\n\napp.post(\n  '/status',\n  handleError(async (req, res) => {\n    const fileId = req.get('X-Actual-File-Id');\n    if (!!fileId) {\n      if (!isValidFileId(fileId)) {\n        res.status(400).send({\n          status: 'error',\n          reason: 'invalid-file-id',\n          details: 'invalid fileId',\n        });\n        return;\n      }\n\n      if (!canAccessFile(fileId, res.locals.user_id)) {\n        res.status(403).send({\n          status: 'error',\n          reason: 'file-access-denied',\n          details: \"You don't have permissions over this file\",\n        });\n        return;\n      }\n    }\n\n    const source = pluggyaiService.getCredentialSource(fileId);\n\n    res.send({\n      status: 'ok',\n      data: {\n        configured: !!source,\n        source,\n      },\n    });\n  }),","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-pluggyai/app-pluggyai.js#L22-L58","documentation":"POST /status passed the fileId format check but canAccessFile(fileId, res.locals.user_id) returned false, so the server responds HTTP 403 with reason 'file-access-denied'. The authenticated user token does not own or have access to the referenced budget file. This is an authorization failure, not a formatting problem.","triggerScenarios":"POST /status with a well-formed X-Actual-File-Id belonging to a different user's budget than the one implied by the request's auth token.","commonSituations":"Using an API token issued for a different Actual account; switching budgets and reusing a stale fileId; multi-user server where the file belongs to another user; token/user mismatch after re-creating the budget (new id).","solutions":["Re-authenticate with a token for the user that owns the budget, or log in to the correct account in the client","Use the fileId of a budget the current user actually owns","Re-upload/sync the budget for this user so a matching files.users row exists","Check the files and users_access tables (or equivalent) to confirm ownership mapping"],"exampleFix":"// before: token for user A, fileId of user B's budget\n// after: log in as user B or use a fileId owned by user A","handlingStrategy":"validation","validationCode":"// verify ownership client-side before calling:\nconst files = await fetch('/files/list', { headers: authHeaders }).then(r => r.json());\nconst owned = files.data?.some(f => f.fileId === fileId);\nif (!owned) throw new Error('Current user cannot access this fileId');","typeGuard":"function canUserAccessFile(fileId, ownedFiles) {\n  return Array.isArray(ownedFiles) && ownedFiles.some(f => f?.fileId === fileId);\n}","tryCatchPattern":"const res = await fetch('/pluggyai/status', { headers: { 'X-Actual-File-Id': fileId, ...authHeaders } });\nif (res.status === 403 && (await res.json()).reason === 'file-access-denied') {\n  await reauthenticateAsOwner(); // or switch to the owning user's budget\n}","preventionTips":["Re-login after switching Actual accounts","Verify the token and fileId come from the same user/session","After recreating a budget, use its new fileId","On multi-user servers, confirm file ownership before API calls"],"tags":["http-403","authorization","pluggyai","permissions"],"backgroundTag":"file-access-denied","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}