Skip to main content
Drupal is a registered trademark of Dries Buytaert
Release: Configuration Language Lock 1.0.2 Minor update available for module config_language_lock (1.0.2). Release: Canvas Override 1.0.1 Minor update available for module canvas_override (1.0.1). Release: Media Remote Image 8.x-1.2 Minor update available for module media_entity_remote_image (8.x-1.2). Release: IDNA Convert Service (punycode) 2.0.4 Minor update available for module idna (2.0.4). Release: Bootstrap Cloud 7.1.3 Minor update available for theme bootstrap_cloud (7.1.3). Release: synship 2.0.1 Minor update available for module synship (2.0.1). Release: Droost 2.0.0-alpha5 New alpha version released for module droost (2.0.0-alpha5). Release: FlowDrop 2.6.0-alpha2 New alpha version released for module flowdrop (2.6.0-alpha2). Usage Milestone: Term CSV Export Import Module term_csv_export_import crossed 1,000 active installs. Module Revived: Year/Month Widget 1.1.3 Module year_month_widget updated after 9 months of inactivity (1.1.3).

Field Guard

3 sites No security coverage Drupal 10–11
View on drupal.org

Field Guard restricts access to specific fields based on custom permissions, ensuring that users can only view or edit them if explicitly granted. It prevents even administrators from accessing guarded fields unless they have been specifically assigned the required permission through configuration. This module is driven entirely by configuration, offering a granular way to manage field-level security.

Field Guard denies a named field to anyone who has not been explicitly granted the permission you nominate — including administrators and user 1. It is driven entirely from configuration, defines no permissions of its own, and only ever denies. It never grants.

It ships with an empty map, so installing it changes nothing until you configure it.

Two problems motivate it, both easy to get wrong by hand.

Definition-level checks fail open

JSON:API and Views ask a different question from "may this user read this value?" They ask "may this field be filtered or sorted on at all?" — and they ask it with no entity in scope.

  • Drupal\views\Plugin\views\field\EntityField::access() calls fieldAccess('view', $definition, $account). $items is omitted, so it is NULL.
  • Drupal\jsonapi\Context\FieldResolver::getFieldAccess() calls fieldAccess('view', $definition, NULL, NULL, TRUE), with $items explicitly NULL.

An implementation that returns neutral() for that case leaves the Views handler in place and permits the JSON:API filter. The value is never rendered, but it can be probed — an exposed date filter binary-searches it, and a sort orders by it.

Field Guard denies. If a caller will not say which entity it means, it does not get the field.

Note the consequence: guarded fields become unfilterable and unsortable for everyone, including permission holders. With no entity in scope there is no way to distinguish a legitimate filter from a probe. That is usually what you want for a field you deliberately guarded, but it is a behaviour change, and it is pinned by a test rather than left to chance.

Checking hasPermission() silently exempts your administrators

This is the subtle one.

hook_entity_field_access() returning AccessResult::forbidden() genuinely cannot be overridden. EntityAccessControlHandler::fieldAccess() contains no admin check at all, folds hook results with orIf(), and forbidden is contagious — so the verdict holds against administer users, an is_admin role, and user 1.

But that only matters once you have decided to forbid. If your deny condition tests hasPermission() on the account, administrators and user 1 pass it every time, because SuperUserAccessPolicy and is_admin roles grant every permission by definition. You never reach the forbidden branch, and the contagion never fires.

Field Guard therefore walks the account's roles, skips is_admin roles, and asks each remaining role directly. Access must be named in a role's own configuration.

The practical effect is separation of duty: granting access is a visible line in a configuration diff, reviewed like any other change, rather than something inherited by being an administrator.

Configuration

protected:
  profile:
    compliance_record:
      field_evidence_date:
        view: 'view compliance evidence'
        edit: 'record compliance evidence'

The nesting is entity type, then bundle, then field name, then operation, then the permission required for it.

  • view and edit are the only operations Drupal's field access API passes. Anything else is treated as unprotected, so a future core operation cannot lock a site out of its own data.
  • An omitted operation is not protected. Omission is how you leave a field open.
  • An empty permission string is treated as unset, not as a permission nobody holds — a total denial that is invisible in config is very hard to diagnose.

Configuration rather than a hardcoded array is deliberate: it is diffable and reviewable, and on a deploy that runs drush config:import it is reverted to the repository on every release, so a live edit that widens access does not survive.

What it does not cover

These are properties of Drupal, not gaps in this module, and they are true of every field-access approach:

  • Programmatic reads. Loading an entity and reading the field value in code, or via Drush, never calls fieldAccess().
  • Views filters, sorts and arguments on unguarded fields. HandlerBase::access() returns TRUE unconditionally; guarding at definition level is the only lever available.
  • Anything below the entity API. Direct SQL sees everything.
  • Audit. Drupal does not log field reads. ProtectedFieldMap::isProtected() is provided as the seam for a consumer that wants to record them.

Relationship to Field Permissions

Field Permissions solves an overlapping problem with a per-field UI and three modes. Field Guard is deliberately narrower: no UI, one mechanism, driven from configuration.

The substantive difference is the first section above — Field Permissions returns neutral() when $items is empty, which is the JSON:API filter and Views handler-removal path. That is tracked upstream as #3003914, reported in 2018 and confirmed with a reproduction in 2026. If it lands, the gap narrows considerably — choose on whether you want a UI or a config file.

Requirements

Drupal 10.6, 11.3 or later, and core's Field and User modules. The supported range tracks the oldest Drupal branch still supported upstream and is exercised in CI against each supported major.

Maintainers

Maintained by Jeremy Michael Cerda, sponsored by Wilkes & Liberty, LLC.

Depends on

Dependencies of the latest stable release

  • field Drupal core
  • user Drupal core

Required by

Tracked projects that depend on this one

No tracked projects depend on this one yet.

Activity

Tracked releases
5
Tracked since
Jul 2026
Latest release
2 weeks ago
Releases (12 mo)
5 ▲ from 0
Maintenance
Active

Release Timeline

Releases

Version Type Core Release date
1.2.0 Stable 10–11 Aug 27, 2026
1.1.0 Stable 10–11 Aug 2, 2026
1.0.1 Stable 10–11 Jul 30, 2026
1.x-dev Dev 10–11 Jul 29, 2026
1.0.0 Stable 10–11 Jul 29, 2026