{"record":{"id":"545255e6119c5343","repo":"actualbudget/actual","slug":"forbidden-545255","errorCode":"forbidden","errorMessage":"permission-not-found","messagePattern":"permission-not-found","errorType":"http","errorClass":null,"httpStatus":403,"severity":"warning","filePath":"packages/sync-server/src/app-admin.js","lineNumber":49,"sourceCode":"// (wont-fix). Actual's multi-user/OpenID feature is intended for friends &\n// family setups, not SaaS, so the attack surface is low. The endpoint is also\n// used in the budget ownership transfer flow, where neither the current nor the\n// target user is necessarily an admin — adding isAdmin would break that flow\n// without a substantial refactor.\napp.get('/users/', validateSessionMiddleware, (req, res) => {\n  const users = UserService.getAllUsers();\n  res.json(\n    users.map(u => ({\n      ...u,\n      owner: u.owner === 1,\n      enabled: u.enabled === 1,\n    })),\n  );\n});\n\napp.post('/users', validateSessionMiddleware, async (req, res) => {\n  if (!isAdmin(res.locals.user_id)) {\n    res.status(403).send({\n      status: 'error',\n      reason: 'forbidden',\n      details: 'permission-not-found',\n    });\n    return;\n  }\n\n  const { userName, role, displayName, enabled } = req.body || {};\n\n  if (!userName || !role) {\n    res.status(400).send({\n      status: 'error',\n      reason: `${!userName ? 'user-cant-be-empty' : 'role-cant-be-empty'}`,\n      details: `${!userName ? 'Username' : 'Role'} cannot be empty`,\n    });\n    return;\n  }\n","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-admin.js#L31-L67","documentation":"HTTP 403 from POST /users (create user). The request passed `validateSessionMiddleware`, but `isAdmin(res.locals.user_id)` is false, so user creation — an admin-only operation — is rejected with `reason:'forbidden', details:'permission-not-found'`.","triggerScenarios":"An authenticated non-admin user calls POST /users to create a new account. Same guard also applies to PATCH /users and DELETE /users.","commonSituations":"Automation using a service token belonging to a regular user; admin role revoked from the account the script was set up with; OpenID-managed users where nobody was granted the admin role.","solutions":["Call POST /users with an admin user's token.","Grant the admin role to the intended user via an existing admin session or directly in the account database.","Bootstrap the first owner/admin user if no admin exists (fresh multi-user setup), then use that account for administration."],"exampleFix":"// before\nawait api.post('/users', payload, { headers: { 'X-ACTUAL-TOKEN': memberToken } });\n// after\nawait api.post('/users', payload, { headers: { 'X-ACTUAL-TOKEN': adminToken } });","handlingStrategy":"validation","validationCode":"const v = await get('/validate', { headers: authHeaders(token) });\nif (v.data.data.permission !== 'admin') throw new Error('User creation requires an admin session');","typeGuard":"function isAdminSession(session) {\n  return session != null && session.permission === 'admin';\n}","tryCatchPattern":"try {\n  await post('/users', payload, { headers: authHeaders(token) });\n} catch (e) {\n  if (e.response?.status === 403 && e.response.data.details === 'permission-not-found') {\n    throw new AdminRequiredError('Use an admin token to create users');\n  }\n  throw e;\n}","preventionTips":["Verify /validate permission === 'admin' before any user-management call","Keep the bootstrap owner account dedicated to administration","If a role downgrade breaks automation, re-provision an admin token"],"tags":["authorization","http-403","forbidden","admin-only"],"backgroundTag":"insufficient-permissions","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}