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

  1. Verify the fileId in the request body belongs to the authenticated user (check via UserService.checkFilePermission or the UI file list).
  2. Log in as an admin account or grant the current user permission over the file first.
  3. 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

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.