{"record":{"id":"a5ce49a71da22f4e","repo":"actualbudget/actual","slug":"not-admin","errorCode":"not-admin","errorMessage":"You have to be admin to manage global secrets","messagePattern":"You have to be admin to manage global secrets","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"packages/sync-server/src/app-secrets.js","lineNumber":69,"sourceCode":"    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\n  res.status(200).send({ status: 'ok' });\n});\n\napp.delete('/:name', async (req, res) => {\n  const name = req.params.name;\n  const fileId = req.get('X-Actual-File-Id');\n  const perBudgetFile = fileId != null;\n","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-secrets.js#L51-L87","documentation":"Global secrets (POST /secrets without an X-Actual-File-Id header) can only be managed by admin users; canManageGlobalSecrets(userId) delegates to isAdmin. A non-admin authenticated user attempting to set a server-wide secret gets HTTP 403 with reason 'not-admin'.","triggerScenarios":"POST /secrets with no X-Actual-File-Id header while authenticated as a regular (non-admin) user; automation using a non-admin token to configure global provider credentials; header accidentally omitted, downgrading a per-budget write to a global write.","commonSituations":"Self-hosted instances where the operator's account was never promoted to admin; scripts sharing one token across admins and regular users; forgetting the header so an intended per-budget secret is treated as global.","solutions":["Authenticate as an admin user for global secret writes.","Promote the operating account to admin in the server's user management if it should manage global secrets.","Include the X-Actual-File-Id header if the secret is meant to be per-budget instead of global.","Split automations: use an admin token for global secrets and a user token for file-scoped ones."],"exampleFix":"// before\n// regular user token, no file header\nPOST /secrets {\"name\":\"gocardless_secret_id\",\"value\":\"...\"} // 403 not-admin\n// after\nPOST /secrets -H 'X-Actual-File-Id: <budgetId>' {\"name\":\"gocardless_secret_id\",\"value\":\"...\"} // per-budget, owner allowed","handlingStrategy":"validation","validationCode":"function assertGlobalSecretAllowed(user) {\n  if (!user.isAdmin) throw new Error('Global secrets require an admin account');\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch('/secrets', { method: 'POST', body: JSON.stringify({ name, value }) });\nif (res.status === 403 && (await res.json()).reason === 'not-admin') {\n  // switch to an admin session or add X-Actual-File-Id to scope per-budget\n}","preventionTips":["Reserve a dedicated admin token for global secret management","Always include X-Actual-File-Id for per-budget secrets to avoid accidental global scope","Promote operator accounts to admin only intentionally","Document which credentials in your tooling are admin-scoped"],"tags":["http-403","authorization","admin","secrets"],"backgroundTag":"insufficient-permissions","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}