Using user_access() or AccessResult for custom access checksfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Route-level _permission checks answer one question well: "does this user hold a named permission?" But real access decisions are often messier than that — "can this user edit their own comment, but not someone else's?", "is this feature even enabled right now?" Those need actual PHP logic, not a YAML string. This lesson covers the two tools Drupal gives you for exactly that: the simple user_access() function, and the richer, cacheable AccessResult object.

What you'll learn in this lesson

  • When a simple user_access() check is enough, and when it isn't
  • Why AccessResult exists at all instead of just returning true or false
  • The three possible outcomes of an access check, and why "neutral" matters
  • How to attach cache metadata to an access decision so it doesn't get served to the wrong user

The simple case: user_access()

if (\Drupal::currentUser()->hasPermission('access simple page')) {
  // Proceed with logic available to users with this permission.
}

Modern Drupal code almost always reaches for \Drupal::currentUser()->hasPermission() (an instance method) rather than the older procedural user_access() function, but they do the same thing: a straightforward boolean check against the current user's permissions. Use this when you just need a quick yes/no answer inside a method that isn't itself an access-control callback — a controller deciding whether to show an extra button, for instance.

The richer case: AccessResult

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;

public function access(AccountInterface $account): AccessResultInterface {
  return AccessResult::allowedIfHasPermission($account, 'access simple page')
    ->andIf(AccessResult::allowedIf($this->isFeatureEnabled()))
    ->addCacheableDependency($this->config('mymodule.settings'));
}

When you're writing a dedicated access-check callback (referenced from a route's _custom_access key, or an entity access handler), Drupal expects an AccessResultInterface object back, not a plain boolean. This isn't ceremony for its own sake — AccessResult carries cache metadata alongside the decision, so Drupal's render cache knows this result is only valid for as long as the underlying config/permission/entity it depended on stays the same.

Three outcomes, not two

AccessResult distinguishes three states, not just allow/deny: allowed, forbidden, and neutral. Neutral means "this particular check has no opinion" — it's neither an explicit grant nor an explicit denial. This matters because Drupal often combines several access checks together (a module's own logic plus core's), and a neutral result lets another check further down the chain make the actual call, rather than accidentally denying access just because one unrelated check didn't have anything to say about it.

Forbidden always wins. If you combine access results with orIf()/andIf(), an explicit forbidden() result from any single check overrides an allowed() from another — Drupal treats an explicit denial as final, precisely so one module can't accidentally override another module's deliberate security restriction.

Quick check: your access callback returns a plain AccessResult::allowed() with no cache metadata attached, but the decision actually depends on the currently logged-in user. What goes wrong? (Answer: without addCacheableDependency() or an equivalent cache context like 'user', Drupal's render cache may serve that "allowed" result to a completely different user later — a serious access-control bug, not just a display glitch.)

Key takeaways

  • Use \Drupal::currentUser()->hasPermission() for a quick boolean check inside ordinary code.
  • Use AccessResult for dedicated access-check callbacks (_custom_access, entity access handlers) — it carries cache metadata a plain boolean can't.
  • AccessResult has three states — allowed, forbidden, neutral — not just true/false; neutral defers the decision to another check in the chain.
  • An explicit forbidden() always overrides an allowed() when results are combined — denial wins.
  • Always attach the right cache metadata to a dynamic access decision, or it can be served to the wrong user from cache.

Coming up next

Permissions and access control are only one piece of how Drupal lets modules extend the system without touching core code. The next section moves into an even more fundamental mechanism: Implementing Hook System — the naming-convention-based system that lets any module react to, or alter, what any other module is doing.