Summary

This adds a permission gate that was part of the module's original design
intent but was never implemented, and fixes a persistence mismatch between
the admin-facing settings form and the code paths that actually enforce
limits. Both issues were found while debugging the field widget's counter
not rendering in a downstream consumer module.

Problem 1: no permission gate on role eligibility

The module's intent from the start was that a role must hold a specific
permission before an administrator can configure a role-based limit for
it — a safeguard against accidentally capping a role (a built-in
role, or an unrelated custom role) with no deliberate opt-in. This gate
did not exist anywhere in the codebase: no
role_aware_maxlength.permissions.yml, and no check anywhere
in RoleLimitResolver or the settings form.

Problem 2: settings form persists to the wrong storage location

hook_field_widget_third_party_settings_form() builds the
admin UI for configuring limits, embedded in the field widget's own
settings on "Manage form display". Field UI's submit handling
always persists that form's values to the widget's
third-party settings — this is a structural property of how Field
UI processes entity form display saves, not something a hook
implementation can redirect.

However, both the field widget's own rendering
(RoleAwareMaxlengthWidget::formElement()) and the server-side
constraint (RoleAwareMaxlengthConstraintValidator) read
limits from the field definition's third-party settings
instead — deliberately, so that enforcement works regardless of
which widget, form, or non-form code path (REST, migrations, a custom
compose UI) creates the content. The two halves of the module were
reading from two different, unsynchronized locations. An administrator
filling in the settings form would see their values saved, but neither
the counter nor server-side enforcement would ever reflect them.

Proposed resolution

1. New permission: role_aware_maxlength

Added role_aware_maxlength.permissions.yml. This is
deliberately not an "administer" permission — it grants no
access to configure anything. It marks a role as eligible to have
a limit applied to it at all.

2. RoleEligibilityCheckerInterface / RoleEligibilityChecker

A small, single-method service (isEligible(string $roleId): bool)
backed by user_role entity storage, checking
$role->hasPermission('role_aware_maxlength'). Extracted as
its own interface/service rather than folded directly into
RoleLimitResolver, for the reason below.

3. RoleLimitResolver updated, backward-compatibly

RoleLimitResolver was deliberately dependency-free —
its own docblock calls this out as a design property, and its full unit
test suite constructs it with zero arguments. Injecting
EntityTypeManagerInterface directly to check permissions
would have broken that and forced every existing test to start mocking
storage.

Instead, the constructor now accepts an optional, nullable
RoleEligibilityCheckerInterface:

public function __construct(
  private readonly ?RoleEligibilityCheckerInterface $eligibilityChecker = NULL,
) {}

Inside resolve(), a role's configured limit is skipped if a
checker is present and reports the role ineligible:

if ($this->eligibilityChecker !== NULL && !$this->eligibilityChecker->isEligible($roleId)) {
  continue;
}

This check runs at resolution time, not just at config-save
time — so revoking the permission from a role immediately stops any
previously-configured limit for that role from being enforced. Stale
role_limits config left behind after a permission change
cannot silently reactivate. All 9 pre-existing unit tests are unaffected;
3 new tests cover the gating behaviour specifically.

4. Settings form filtered to eligible roles

hook_field_widget_third_party_settings_form() now only
offers role-limit fields for roles that currently hold the permission,
via the same RoleEligibilityCheckerInterface service. If no
role is currently eligible, the form shows a message linking to the
permissions page rather than an empty section.

5. hook_entity_presave() syncs widget-level settings to the field

Added a new hook, implemented for EntityFormDisplayInterface
entities. On every entity form display save, for each component carrying
role_aware_maxlength third-party settings, the corresponding
FieldConfig is loaded and its own third-party settings are
updated (or unset, if the widget-level settings are now empty) to match.

This keeps Field UI's native persistence target (widget-level, where
admins actually interact with the settings form) and the module's actual
enforcement mechanism (field-level) in sync, without requiring any
change to how site builders configure the module — still just the
"Manage form display" settings form, no custom code.

Files changed

  • role_aware_maxlength.permissions.yml — new
  • src/Service/RoleEligibilityCheckerInterface.php — new
  • src/Service/RoleEligibilityChecker.php — new
  • src/Service/RoleLimitResolver.php — optional checker dependency, gating logic in resolve()
  • role_aware_maxlength.services.yml — registers the checker service, wires it into the resolver
  • role_aware_maxlength.module — role list filtered to eligible roles in the settings form; new hook_entity_presave()
  • tests/src/Unit/RoleLimitResolverTest.php — 3 new tests for the gating behaviour

Remaining risk / needs verification

hook_entity_presave() has no test coverage yet — there
is no kernel test exercising "save an entity form display, confirm
the field config picked up the settings." This is the most likely
place for a surprise. A kernel test covering this save-and-sync path
should land before this is considered fully verified.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

  • main Comparechanges, plain diff MR !1

Comments

lisa.rae created an issue. See original summary.

lisa.rae’s picture

Issue summary: View changes
lisa.rae’s picture

Issue summary: View changes

lisa.rae’s picture

Status: Active » Needs work

  • lisa.rae committed 6c7d84aa on main
    Issue #3608275 by lisa.rae:  Create  permission for configurability
    
lisa.rae’s picture

Status: Needs work » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.