Drupal Permissions Explained: Declaring Custom Access with permissions.ymlfor Drupal 11 , and 10

Last updated :  

Last lesson you saw two of the three routes in page_example.routing.yml guarded by permission strings — 'access simple page' and 'access arguments page'. We said those strings "must match a permission declared in a .permissions.yml file." This lesson is that file: where custom permissions actually come from, and how they end up as real, clickable checkboxes on Drupal's permissions admin page.

Why modules need to declare their own permissions

Drupal ships with a set of core permissions ("access content", "administer users", and so on), but the moment your module adds its own protected functionality, core has no idea that functionality exists. You have to tell Drupal: "here is a new permission, here's its name, here's what it does, please make it available for site administrators to grant to roles." That declaration happens in a file named [module_name].permissions.yml.

What you'll learn in this lesson

  • The mandatory naming convention Drupal uses to auto-discover a module's permissions
  • What a permission's machine name, title, and description each do
  • How that machine name connects back to the _permission keys you saw in routing.yml
  • Two optional keys — restrict access and permission_callbacks — for more advanced cases

The source file

Path: modules/page_example/page_example.permissions.yml

# Since the access to our new custom pages will be granted based on special
# permissions, we need to define what those permissions are here. This ensures
# that they are available to enable on the permissions administration pages.

'access simple page':
  title: Access simple page
  description: Allow users to access simple page

'access arguments page':
  title: Access page with arguments
  description: Allow users to access page with arguments

Two permissions, four lines each. That's the entire file — and it's all Drupal needs.

How it works

The mandatory file name

Drupal finds permissions by looking for a file literally named [module_name].permissions.yml, sitting right next to your module's .info.yml file. Get the name wrong — even a small typo — and Drupal simply won't discover any permissions from it, silently. There's no error message, the permissions just never show up. No PHP registration is required beyond having this one correctly-named YAML file.

The permission machine name (the YAML key)

'access simple page':

This top-level key is the permission's machine name — the exact string every other part of the codebase uses to reference it programmatically. A few rules worth internalizing:

  • It must be unique across every enabled module on the site, not just your own.
  • By convention it's written in lowercase with spaces (some contributed modules use underscores instead — both work, spaces are just the more common Drupal convention).
  • It's wrapped in single quotes here specifically because it contains spaces; YAML requires quoting any key with spaces or special characters in it.
  • Once real sites have configured roles against this string, changing it later is a breaking change — every role assignment referencing the old string effectively vanishes.

This is exactly the string you saw in the previous lesson's routing file:

requirements:
  _permission: 'access simple page'

And it's the same string you'd pass to a PHP access check anywhere in your own code, e.g. \Drupal::currentUser()->hasPermission('access simple page').

title: the human-readable label

title: Access simple page

This is the label a site administrator actually sees on the Permissions page (/admin/people/permissions). Keep it short, descriptive, and in sentence case. It's passed through Drupal's translation layer automatically — no extra work needed to make it translatable.

description: context for the administrator

description: Allow users to access simple page

Displayed underneath the title in the admin UI, in smaller text. A good description answers one specific question for the person granting this permission: "If I check this box for a role, what exactly can users in that role now do?" Like title, it's automatically translatable.

A file can declare as many permissions as you need

Notice the two permissions here are just two top-level keys in the same file, each with its own title and description. There's no limit — a module with a dozen protected features would declare a dozen permissions the same way. These two specifically map to the two custom routes you saw last lesson:

  • 'access simple page' guards /examples/page-example/simple.
  • 'access arguments page' guards /examples/page-example/arguments/{first}/{second}.

Two optional keys you'll meet later

This particular file keeps things simple and doesn't use either of these, but you'll run into them in real modules:

  • restrict access: true — displays a red security warning next to the permission in the admin UI, signaling "only grant this to roles you fully trust." Common for anything administrative or sensitive.
  • provider — normally inferred automatically from the filename, but can be set explicitly in unusual cases.

There's also a more advanced pattern for permissions that can't be listed statically — say, one permission generated per content type on the site. For that, a module points a permission_callbacks key at a PHP method that generates the permission list at runtime instead of hardcoding it in YAML. You won't need this for a while, but it's worth knowing it exists.

How Drupal actually loads this: when the cache rebuilds (or the module is first installed), Drupal's PermissionHandler service scans every enabled module for *.permissions.yml files, reads them, and registers whatever it finds. That's the same caching mechanism you were warned about with hooks — if you add a new permission and it doesn't show up, clear the cache before you start debugging anything else.

See it for yourself

Visit /admin/people/permissions on your own DDEV site and search for "access simple page" and "access page with arguments." You'll find both, filed under the "Page Example" module heading, with exactly the title and description text from the YAML file above.

The Permissions admin page showing Access simple page and Access page with arguments under the Page Example module

Quick check: if you renamed the YAML key from 'access simple page' to 'view simple page' after this module was already live on a production site with roles configured, what would happen to those existing role assignments? (Answer: they'd silently stop working — the old string no longer matches anything, and any role that had this permission checked effectively loses it. This is exactly why permission machine names are treated as a stable, load-bearing contract once shipped.)

Key takeaways

  • The file must be named [module_name].permissions.yml and sit in the module root — Drupal auto-discovers it with zero additional registration.
  • Each top-level YAML key is the permission's machine name, referenced by routing's _permission requirement and by PHP calls like hasPermission().
  • title is the human-readable label on /admin/people/permissions; both title and description are automatically translatable.
  • description tells administrators what granting the permission actually allows — write it from their point of view.
  • A single file can declare many permissions; each maps to whatever access checks your module needs.
  • For permissions that must be generated dynamically (one per content type, for example), use permission_callbacks instead of static YAML entries.

Coming up next

You've now seen how a route gets defined and how a permission gets declared — but so far, the only code that actually runs and produces a page is a method name mentioned in YAML. Next, we open PageExampleController.php itself and look at the real PHP class behind description(), simple(), and arguments() — the code that finally turns a matched route into an actual page a visitor can read.