{"record":{"id":"ee18576dca7456a9","repo":"actualbudget/actual","slug":"file-access-denied-ee1857","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-secrets.js","lineNumber":61,"sourceCode":"      status: 'error',\n      reason: 'invalid-secret-name',\n      details: 'Unknown secret name',\n    });\n    return;\n  }\n\n  if (perBudgetFile) {\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 (!canManagePerBudgetFileSecrets(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  } else if (!canManageGlobalSecrets(res.locals.user_id)) {\n    res.status(403).send({\n      status: 'error',\n      reason: 'not-admin',\n      details: 'You have to be admin to manage global secrets',\n    });\n    return;\n  }\n\n  const secretFileId = perBudgetFile ? fileId : null;\n  secretsService.set(name, value, secretFileId);\n","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-secrets.js#L43-L79","documentation":"For per-budget secret writes (POST /secrets with X-Actual-File-Id), the server checks canManagePerBudgetFileSecrets(fileId, user_id), which is true only for admins or users with granted permission over that budget file. Authenticated users without ownership of the file are rejected with HTTP 403 and reason 'file-access-denied'.","triggerScenarios":"POST /secrets with X-Actual-File-Id of a budget owned by another user, authenticated as a non-admin session; a user whose file permission was revoked (granted = 0) trying to update that budget's secrets; using the wrong file id that points at someone else's budget.","commonSituations":"Multi-user sync-server setups where a teammate tries to configure bank-sync credentials for a budget they don't own; session user changed after access was revoked; automation running under a service account that is not admin and not owner.","solutions":["Log in as the owner of the budget file or as an admin user before making the call.","Have an admin grant the user permission over the file (user access management for that budget).","Verify the X-Actual-File-Id is the budget you actually have access to, not another one.","Store the secret globally as admin if it should be shared across budgets."],"exampleFix":"// before\n// non-owner session\nfetch('/secrets', { method: 'POST', headers: { 'X-Actual-File-Id': otherUsersBudgetId }, ... }) // 403\n// after\n// authenticate as the file owner or an admin, or get granted access first, then retry the same request","handlingStrategy":"validation","validationCode":"function canEditBudgetSecrets(user, fileId) {\n  return user.isAdmin || user.ownedFileIds.includes(fileId);\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch('/secrets', { method: 'POST', headers: { 'X-Actual-File-Id': fileId }, ... });\nif (res.status === 403 && (await res.json()).reason === 'file-access-denied') {\n  // prompt for owner/admin credentials or request file access\n}","preventionTips":["Verify the session user owns the target budget before writing file-scoped secrets","Use admin tokens only for admin operations","Re-check permissions after access changes/offboarding","Keep automation credentials tied to the budgets they manage"],"tags":["http-403","authorization","multiuser","secrets"],"backgroundTag":"access-denied","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}