Checking permissions in form access callbacksfor Drupal 8 , 9 , 10 , and 11

Last updated :  

A locked door only matters if someone actually checks the key before letting people through. In Drupal, that check happens at the route level — before your form's buildForm() ever runs, the routing system decides whether the current visitor is even allowed to reach it. This lesson covers the real mechanism: requirements in a route's .routing.yml entry, not anything defined inside the form class itself.

What you'll learn in this lesson

  • Why form access is controlled at the route, not inside the form class
  • The three real requirement keys: _permission, _role, and _custom_access
  • How to define a custom permission and grant it to roles

Access is a routing.yml requirement, not a form method

The simplest and most common check — a single named permission — goes straight in the route definition:

my_module.secure_form:
  path: '/secure-form'
  defaults:
    _form: '\Drupal\my_module\Form\SecureForm'
    _title: 'Secure Form'
  requirements:
    _permission: 'access secure form'

And the form class itself stays completely ordinary — no access-related code inside it at all:

namespace Drupal\my_module\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class SecureForm extends FormBase {

  public function getFormId() {
    return 'secure_form';
  }

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['message'] = [
      '#markup' => $this->t('Welcome to the secure form!'),
    ];
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];
    return $form;
  }

  public function submitForm(array &$form, FormStateInterface $form_state) {
    $this->messenger()->addMessage($this->t('Form submission successful!'));
  }

}
Corrected from the original: there is no built-in static access($access_checker, $user) method on FormBase — that pattern doesn't exist in Drupal's Form API. Access control for a form-based route lives in the route's requirements key, exactly like it does for a controller route. Also swapped drupal_set_message() — removed in Drupal 8 — for $this->messenger()->addMessage().

Defining and granting the permission

A permission referenced in _permission must be declared in your module's *.permissions.yml file before it means anything:

access secure form:
  title: 'Access Secure Form'
  description: 'Allows users to access the secure form.'
  restrict access: TRUE

restrict access: TRUE adds a warning icon next to this permission on the admin Permissions page — a signal to site builders that granting it carries real risk, worth using for anything beyond routine content-editing access. Once declared, an administrator assigns it to roles at People → Permissions, and Drupal checks it automatically on every request to that route — no code runs to re-check it.

Beyond a single permission

Two other requirement keys cover cases a single permission can't:

  • _role: 'administrator' — restrict by role name directly, rather than a permission a role happens to hold.
  • _custom_access: '\Drupal\my_module\Access\SecureFormAccessCheck::access' — a callback for logic a static permission check can't express, such as "only the entity's own owner, or an administrator."

Quick check: your form should be reachable by any authenticated user who owns the content it edits, but never by other authenticated users, regardless of their permissions. Which requirement key fits — _permission, _role, or _custom_access? (Answer: _custom_access — "owns this specific content" is a per-request, per-entity decision that a static permission or role name can't express on its own; it needs a callback that inspects the current user and the specific entity being accessed.)

Key takeaways

  • Form access is enforced by the routing system before buildForm() runs — there's no access method to define inside the form class itself.
  • _permission handles the common case: a single named permission, declared in *.permissions.yml and granted to roles via the admin UI.
  • Use _role to restrict by role name directly, and _custom_access for logic that depends on more than a static permission check (like entity ownership).
  • restrict access: TRUE in a permission's definition flags it as sensitive in the admin UI — use it for anything beyond routine access.
  • drupal_set_message() is dead Drupal 7 code — use the Messenger service instead.

Coming up next

Permissions gate who can reach a form at all — but a form that accepts file uploads has its own, narrower category of risk regardless of who's submitting it. Next, we'll look at securing file uploads specifically: validating extensions, size limits, and where uploaded files actually get stored.