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

  1. Log in as the owner of the budget file or as an admin user before making the call.
  2. Have an admin grant the user permission over the file (user access management for that budget).
  3. Verify the X-Actual-File-Id is the budget you actually have access to, not another one.
  4. 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

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.