actualbudget/actual · error
file-denied
file-denied
Error message
You don't have permissions over this file
What it means
Thrown by the POST /access endpoint in the Actual sync-server admin app when the authenticated session user has no granted permission over the target fileId and is not an admin. The server checks UserService.checkFilePermission(fileId, session.user_id); if granted is 0 (or undefined) and isAdmin fails, it rejects the access-grant request with reason 'file-denied'. It means the caller is trying to share a file they do not own or manage.
Source
Thrown at packages/sync-server/src/app-admin.js:239
res.json(accesses);
});
app.post('/access', (req, res) => {
const userAccess = req.body || {};
const session = validateSession(req, res);
if (!session) return;
const { granted } = UserService.checkFilePermission(
userAccess.fileId,
session.user_id,
) || {
granted: 0,
};
if (granted === 0 && !isAdmin(session.user_id)) {
res.status(400).send({
status: 'error',
reason: 'file-denied',
details: "You don't have permissions over this file",
});
return;
}
const fileIdInDb = UserService.getFileById(userAccess.fileId);
if (!fileIdInDb) {
res.status(404).send({
status: 'error',
reason: 'invalid-file-id',
details: 'File not found at server',
});
return;
}
if (!userAccess.userId) {View on GitHub (pinned to d4334cb6e6)
Solutions
- Verify the fileId in the request body belongs to the authenticated user (check via UserService.checkFilePermission or the UI file list).
- Log in as an admin account or grant the current user permission over the file first.
- Fix client-side state so the correct fileId is sent; clear stale cached budgets.
Example fix
// before
await fetch('/access', { method: 'POST', body: JSON.stringify({ fileId: staleId, userId }) });
// after
const { granted } = await checkFilePermission(fileId, currentUserId);
if (granted) await fetch('/access', { method: 'POST', body: JSON.stringify({ fileId, userId }) }); Defensive patterns
Strategy: validation
Validate before calling
async function canShare(fileId, userId, session) {
const { granted } = await checkFilePermission(fileId, session.user_id) || { granted: 0 };
return granted > 0 || session.isAdmin;
}
// call before POST /access Type guard
function hasPermission(p) {
return typeof p === 'object' && p !== null && typeof p.granted === 'number' && p.granted > 0;
} Try / catch
try {
await api.post('/access', { fileId, userId });
} catch (e) {
if (e.response?.data?.reason === 'file-denied') {
throw new Error(`No permission over file ${fileId}; use an owner or admin account`);
}
throw e;
} Prevention
- Always verify permission over the fileId with the current session before sharing.
- Use admin credentials for administrative sharing scripts.
- Keep client-side file ids in sync with the server file list.
When it happens
Trigger: POST /access with a body {fileId, userId} where the session user has no users_files row granting permission on fileId; passing a wrong/typo'd fileId the user never had access to; calling while logged in as a non-admin user for a file owned by someone else.
Common situations: A manager user tries to share a budget file owned by another account; a client caches a stale fileId after the file was re-uploaded under a different id; scripts run with a personal (non-admin) token attempting bulk sharing.
Related errors
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/847860ae3d232a9d.
Report an issue: GitHub.