Drupal is a registered trademark of Dries Buytaert
File Entity (fieldable files) 8.x-2.7 Minor update available for module file_entity (8.x-2.7). Field Group 4.1.0-alpha4 New alpha version released for module field_group (4.1.0-alpha4). CKEditor 5 Plugin Pack 1.5.4 Minor update available for module ckeditor5_plugin_pack (1.5.4). CKEditor 5 Premium Features 1.8.3 Minor update available for module ckeditor5_premium_features (1.8.3). Drupal Canvas 1.9.0 Minor update available for module canvas (1.9.0). CKEditor 4 LTS - WYSIWYG HTML editor 1.0.5 Minor update available for module ckeditor_lts (1.0.5). Token Content Access 3.1.2 Minor update available for module tca (3.1.2). Islandora Mirador 3.0.0 Major update available for module islandora_mirador (3.0.0). Configuration development 8.x-1.12 Module config_devel updated after 8 months of inactivity (8.x-1.12). Provus Mega Menu Module provus_mega_menu crossed 1,000 active installs.

Field Guard

No security coverage
View on drupal.org

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.

Activity

Total releases
2
First release
Jul 2026
Latest release
1 hour ago
Releases (12 mo)
2 ▲ from 0
Maintenance
Active

Releases

Version Type Release date
1.x-dev Dev Jul 29, 2026
1.0.0 Stable Jul 29, 2026