ObjectStackObjectStack

Field-Level Security

Control visibility and editability of specific fields — readable/editable rules, hidden vs. read-only semantics, and server-side enforcement of declared rules.

Field-Level Security

Control visibility and editability of specific fields.

Field Permissions in Permission Sets

Field permissions are keyed <object>.<field> and use readable / editable:

fields: {
  // Read-only: visible but not editable
  'account.annual_revenue': { readable: true, editable: false },
  'account.description': { readable: true, editable: true },
  // Hidden: not visible at all
  'account.ssn': { readable: false, editable: false },
  'opportunity.amount': { readable: true, editable: true },
  'opportunity.probability': { readable: true, editable: false },
}

Hidden vs. Read-Only

// Hidden: Field not visible at all
{ readable: false, editable: false }

// Read-Only: Field visible but not editable
{ readable: true, editable: false }

// Editable: Field visible and editable
{ readable: true, editable: true }

Server-side enforcement of declared rules

The client-side ObjectForm / inline grid hides non-editable fields from the UI — but that is a UX layer only. The SecurityPlugin middleware enforces field-level write rules on the server, regardless of how the request arrived (REST, GraphQL, raw ObjectQL call).

On readfind / findOne results have non-readable fields stripped from every record before the response leaves the engine.

On writeinsert / update requests are checked before the operation reaches the driver. If the payload contains any field the caller is not permitted to edit, the engine throws PermissionDeniedError (mapped to HTTP 403) with the offending field names in details.forbiddenFields:

{
  "error": {
    "code": "PERMISSION_DENIED",
    "message": "[Security] Field write denied: not permitted to edit [salary, ssn] on 'employee'",
    "details": {
      "operation": "insert",
      "object": "employee",
      "forbiddenFields": ["salary", "ssn"]
    }
  }
}

Why throw instead of silently stripping? Silent strip hides the security boundary from honest clients (their update "doesn't save" and they cannot tell why) AND gives a probing client no signal that the field exists. Throwing makes the boundary observable in both directions — legitimate UIs get an actionable error to fix; probing clients learn nothing they could not already infer.

Default-visible (block-list) semantics. Fields without an explicit rule pass through untouched — readable and writable. Permission sets only constrain fields they explicitly enumerate, and field grants union most-permissively across a user's sets (one set's readable: true out-votes another set's false). Until a subtractive muting layer lands (ADR-0066 ⑧), a { readable: false } rule masks the field only as long as no other set the user holds declares it readable: true — so protect sensitive fields by granting them only in the sets that need them, and treat a sensitive field on a broadly-granted object as a review smell — see the FLS posture warning. Declared rules themselves are enforced fail-closed: a masked field is stripped on read and a write to a non-editable field throws.

Bulk inserts. Arrays are checked row-by-row; a single offending field in any row rejects the whole batch atomically.

System operations. ExecutionContext { isSystem: true } bypasses the check entirely — used for migrations, seed loading, and audit log writes.


See also

On this page