Problem/Motivation

The Routing Access report (RoutingAccessComparison::compare() /
RoutingAccessForm) only evaluates routes gated by
_permission, _role, or
_user_is_logged_in. Any route without one of those three
requirements is silently dropped from the report entirely, including:

  • Publicly accessible routes using _access: 'TRUE' (a
    statically knowable case that should be reported as accessible to both
    roles), and
  • Routes with genuinely dynamic access checks such as
    _entity_access or _custom_access, which
    can't be resolved without a request context.

In an audit tool, silently omitting routes is misleading — an administrator
comparing two roles has no way to know the report is incomplete, or by how
much (on a default Standard install, roughly 169 of the site's routes fall
into this category).

Investigating this also surfaced two related bugs in the same report:

  • Unescaped HTML in #markup.
    RoutingAccessForm and PermissionsForm build list
    items by concatenating route/permission machine names, paths, and
    provider/group names directly into #markup strings with no
    escaping. This is user-visible, not just theoretical: several of Drupal
    core's routes have </> in their names
    (see next point), and browsers silently swallow the resulting
    unrecognized tags, making the affected rows render with a blank title.
  • Core's angle-bracket pseudo-routes are compared as if they were
    real pages.
    system.routing.yml defines
    <front>, <none>,
    <nolink>, <button>, and
    <current> as internal link-generation placeholders, not
    navigable pages. Before this fix they were included in the comparison —
    combined with the escaping bug above, four of them rendered as blank,
    indistinguishable "/" rows in the "shared" column, and
    <current> (which has no access requirement at all)
    inflated the "not analyzable" count.

Proposed resolution

In RoutingAccessComparison:

  • Added isStaticallyAnalyzable(), checked before evaluating
    a route. Routes with none of _permission, _role,
    _user_is_logged_in, or _access are collected into
    a new not_analyzable_routes result key (route name → title /
    path / requirements) instead of being dropped; not_analyzable_count
    is derived from it.
  • checkAccess() now resolves _access: 'TRUE' as
    accessible to both roles directly, and route_info['requirements']
    includes _access when present.
  • compare() now skips any route name starting with
    < up front, excluding core's reserved pseudo-routes
    (<front>, <none>,
    <nolink>, <button>,
    <current>) from every part of the comparison.

In RoutingAccessForm:

  • The not-analyzable count is merged into the existing disclaimer
    message at the top of the report.
  • A collapsed (<details>) "N routes not statically
    analyzable" section, grouped by module and respecting the module filter,
    lists the skipped routes below the three result columns (placed as a
    sibling of the flex-based results container, not inside it, so it can't
    distort the three-column layout).
  • All dynamic values (route name, path, requirements string,
    provider/group name) are now wrapped in Html::escape() before
    being concatenated into #markup.

In PermissionsForm, the same Html::escape()
treatment was applied to the provider/group name and permission machine
name (permission titles/descriptions were left untouched since they're
already-safe TranslatableMarkup from trusted
hook_permission() implementations).

Test coverage: kernel tests added to
tests/src/Kernel/RoutingAccessComparisonTest.php
testPublicAccessRoute, testDynamicAccessRouteIsCountedNotDropped,
and testPseudoRoutesAreExcluded — using new synthetic routes
(role_audit_test.public_route,
role_audit_test.dynamic_access) in the
role_audit_test fixture module.

User interface changes

The Routing Access report's disclaimer now states how many routes could not
be statically analyzed, and a new collapsed "N routes not statically
analyzable" section appears below the three comparison columns, listing
those routes grouped by module.

API changes

RoutingAccessComparison::compare()'s return array gains two new
keys: not_analyzable_count (int) and
not_analyzable_routes (array, same shape as
route_info). No existing keys changed shape.

Issue fork role_audit-3608956

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:

Comments

gedur created an issue. See original summary.

  • gedur committed 213dc092 on 1.x
    Issue #3608956: Routing Access report silently drops routes with dynamic...
gedur’s picture

Status: Needs review » 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.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.