ACL for Your Configuration Section

acl.xml declares a named permission, and the section's resource element is what actually restricts your settings to a role.

Syntax<resource id="Acme_Loyalty::config_loyalty" title="Loyalty Section" translate="title" sortOrder="10"/>

An ACL resource is a named permission, such as Acme_Loyalty::config_loyalty. Admin roles are assembled from these names under System > Permissions > User Roles. A module declares its own in etc/acl.xml.

Two halves, and only one of them protects anything

Declaring the resource puts a checkbox in the role tree. On its own it protects nothing. The protection is the <resource> element inside your <section> in system.xml, naming the same id. Magento then hides the section from any role that does not hold that resource.

Position in the tree

acl.xml is a nested tree and position matters. A configuration section's resource is nested under Magento_Config::config, which sits under Magento_Backend::stores_settings, under Magento_Backend::stores, under the root Magento_Backend::admin. Put it somewhere else and the checkbox turns up in the wrong branch of the role form, where nobody looks for it.

What trips people up

A section with no <resource> element is not open to everyone, it is closed to everyone: Magento\Config\Model\Config\Structure\Element\Section::isAllowed() returns false when the element carries no resource, so the section is missing from Stores > Configuration even for a full-access administrator. A mistyped id fails differently: the ACL holds no such resource, so the check falls back to asking whether the role is allowed everything, which a full-access administrator passes and every restricted role fails. After editing acl.xml or system.xml run bin/magento cache:flush, because the role form reads the resource tree from cache.

Example

Example · xml
<!-- app/code/Acme/Loyalty/etc/acl.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::stores">
                    <resource id="Magento_Backend::stores_settings">
                        <resource id="Magento_Config::config">
                            <resource id="Acme_Loyalty::config_loyalty"
                                      title="Loyalty Section"
                                      translate="title"
                                      sortOrder="10"/>
                        </resource>
                    </resource>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

When to use it

  • Keep a payment or API credential out of reach of staff who only manage the catalog.
  • Give a client their own admin role that excludes the settings you support.
  • Reuse one permission across a configuration section and the admin screens that go with it.
  • Audit which roles can currently see a section before adding a credential field to it.

More examples

The section that points at it

This is the half that enforces anything, and it is also what makes the section appear: Magento\Config\Model\Config\Structure\Element\Section::isAllowed() returns false when the element has no <resource>, so an unguarded section is hidden from every role rather than open to all of them. The string must match the resource id in acl.xml character for character, including the double colon; a typo raises no error, and because the ACL then holds no such resource the check falls back to asking whether the role is allowed everything, so the section stays visible to a full-access administrator while vanishing for every restricted role. After adding it, run bin/magento cache:flush, open System > Permissions > User Roles, untick Loyalty Section for a limited role, and confirm the section has vanished from that user's Stores > Configuration.

Example · xml
<!-- app/code/Acme/Loyalty/etc/adminhtml/system.xml (section opening only) -->
<section id="loyalty" translate="label" type="text" sortOrder="400"
         showInDefault="1" showInWebsite="1" showInStore="1">
    <label>Loyalty</label>
    <tab>general</tab>

    <!-- Without this line the section never renders, for any admin role at all. -->
    <resource>Acme_Loyalty::config_loyalty</resource>
    <!-- groups and fields follow -->
</section>

Guarding an admin controller with the same resource

Magento\Backend\App\AbstractAction declares ADMIN_RESOURCE as Magento_Backend::admin and checks it through _isAllowed(), which calls isAllowed(static::ADMIN_RESOURCE). Redeclaring the constant is the whole mechanism: a role without the resource gets the admin access-denied page instead of your action, with no code of yours involved. Reuse the configuration resource when the screen and the settings belong together; declare a second resource in acl.xml when they should be grantable separately. New classes need bin/magento setup:di:compile in production mode.

Example · php
<?php
// app/code/Acme/Loyalty/Controller/Adminhtml/Points/Reset.php
declare(strict_types=1);

namespace Acme\Loyalty\Controller\Adminhtml\Points;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\Result\JsonFactory;

class Reset extends Action implements HttpPostActionInterface
{
    public const ADMIN_RESOURCE = 'Acme_Loyalty::config_loyalty';

    public function __construct(
        Context $context,
        private readonly JsonFactory $resultJsonFactory
    ) {
        parent::__construct($context);
    }

    public function execute(): Json
    {
        return $this->resultJsonFactory->create()->setData(['reset' => true]);
    }
}

Checking the permission in your own code

Magento\Framework\AuthorizationInterface::isAllowed($resource, $privilege = null) answers for the user in the current session, so it is only meaningful inside an admin request. Both methods here make the identical call. Reach for this when a single screen shows more than one sensitive thing and you want to hide part of it rather than block the whole page; note that it decides what to render, and the controller's ADMIN_RESOURCE is still what stops a direct request.

Example · php
<?php
// app/code/Acme/Loyalty/Acl/SectionAccess.php
declare(strict_types=1);

namespace Acme\Loyalty\Acl;

use Magento\Framework\AuthorizationInterface;

class SectionAccess
{
    public const RESOURCE_ID = 'Acme_Loyalty::config_loyalty';

    public function __construct(
        private readonly AuthorizationInterface $authorization
    ) {
    }

    public function canSeeApiKey(): bool
    {
        // Shorthand.
        return $this->authorization->isAllowed(self::RESOURCE_ID);
    }

    public function canSeeApiKeyLonghand(): bool
    {
        // Longhand: the second argument defaults to null.
        return $this->authorization->isAllowed(self::RESOURCE_ID, null);
    }
}

Discussion

  • Be the first to comment on this lesson.