actualbudget/actual · error
file-access-denied
file-access-denied
Error message
You don't have permissions over this file
What it means
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'.
Source
Thrown at packages/sync-server/src/app-secrets.js:61
status: 'error',
reason: 'invalid-secret-name',
details: 'Unknown secret name',
});
return;
}
if (perBudgetFile) {
if (!isValidFileId(fileId)) {
res.status(400).send({
status: 'error',
reason: 'invalid-file-id',
details: 'invalid fileId',
});
return;
}
if (!canManagePerBudgetFileSecrets(fileId, res.locals.user_id)) {
res.status(403).send({
status: 'error',
reason: 'file-access-denied',
details: "You don't have permissions over this file",
});
return;
}
} else if (!canManageGlobalSecrets(res.locals.user_id)) {
res.status(403).send({
status: 'error',
reason: 'not-admin',
details: 'You have to be admin to manage global secrets',
});
return;
}
const secretFileId = perBudgetFile ? fileId : null;
secretsService.set(name, value, secretFileId);
View on GitHub (pinned to d4334cb6e6)
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.
Example fix
// before
// non-owner session
fetch('/secrets', { method: 'POST', headers: { 'X-Actual-File-Id': otherUsersBudgetId }, ... }) // 403
// after
// authenticate as the file owner or an admin, or get granted access first, then retry the same request Defensive patterns
Strategy: validation
Validate before calling
function canEditBudgetSecrets(user, fileId) {
return user.isAdmin || user.ownedFileIds.includes(fileId);
} Try / catch
const res = await fetch('/secrets', { method: 'POST', headers: { 'X-Actual-File-Id': fileId }, ... });
if (res.status === 403 && (await res.json()).reason === 'file-access-denied') {
// prompt for owner/admin credentials or request file access
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/ee18576dca7456a9.
Report an issue: GitHub.