Restricting a Custom Contao Backend Module to Specific User Groups
Register a custom Contao backend module so it appears in the user-group 'Allowed modules' list, let Contao hide the menu entry automatically, and enforce the same check in your controller to block direct URL access.
08 Sept 2025, 09:39 UTC

The problem and the takeaway
You have added a custom backend module to Contao and now every backend user can see it — or nobody can. Contao already ships a working access-control mechanism for backend modules: modules registered under $GLOBALS['BE_MOD'] automatically appear in the user-group settings as checkboxes, and Contao hides menu entries the current user is not allowed to see. The takeaway: register the module in the standard way, let Contao handle menu visibility, and add one explicit permission check in your code so the module cannot be reached by typing its URL directly.
This guide assumes Contao 4.13 or 5.x with a project-specific bundle or the contao/ directory for configuration. The exact registration API has changed between major versions, so confirm the details against the documentation for your installed version (composer show contao/core-bundle or contao/manager-bundle tells you what is installed).
Prerequisites
- A working Contao installation with shell access to run the console (
vendor/bin/contao-consolein Contao 4.13+, orcontao-consolein older setups). - Administrator access to the Contao backend, so you can edit user groups.
- A place for your code: either an app-level bundle under
src/or a custom extension. Configuration snippets below go into thecontao/directory (Contao 4.13+/5) orapp/Resources/contao/(older 4.x).
How Contao's backend module ACL actually works
Contao maintains a global array $GLOBALS['BE_MOD'] that maps navigation sections (such as content or system) to the modules inside them. For every entry, the backend user object checks whether the module key is present in the user's (or their groups') allowed modules list. If it is not, the menu item is not rendered. The same list is edited in the backend under System → User groups → Allowed modules — you do not need to invent your own permission key for basic visibility.
Two consequences matter for engineering decisions:
- Menu hiding is automatic once the module is registered in
BE_MOD. You get the checkbox in the group settings for free. - Menu hiding alone is not enforcement. If your module is a controller route, a user who guesses the URL could still reach it unless you check permissions in the controller itself.
Registering the module
Create or edit contao/config/config.php and add the module to an existing section (or your own):
<?php
// contao/config/config.php
$GLOBALS['BE_MOD']['content']['my_module'] = [
'tables' => ['tl_my_module'],
];
With a tables entry, Contao routes the module to its built-in data-container engine and expects a DCA for tl_my_module under contao/dca/tl_my_module.php. If your module is a custom controller instead (the common approach in Contao 4.13+/5), register it with the #[AsBackendModule] attribute on the controller class:
<?php
// src/Controller/Backend/MyModuleController.php
namespace App\Controller\Backend;
use Contao\CoreBundle\Controller\AbstractBackendModuleController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
#[Route('%contao.backend.route_prefix%/my-module', name: 'my_module')]
class MyModuleController extends AbstractBackendModuleController
{
// ...
}
The attribute form (#[AsBackendModule]) and the exact base classes differ between Contao 4.13 and 5.x; check the developer documentation for your version before copying. What stays constant is the ACL behaviour described next.
Enforcing the permission in code
The menu entry is hidden automatically, but add an explicit check so direct URL access is rejected. For BE_MOD-registered modules the check uses the backend user's modules permission field:
use Contao\BackendUser;
use Contao\CoreBundle\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security; // or the Security helper in newer Symfony versions
// Inside your controller action, before any business logic:
$user = $this->security->getUser();
if (!$user instanceof BackendUser || !$user->hasAccess('my_module', 'modules')) {
throw new AccessDeniedException('Not allowed to access my_module.');
}
hasAccess('my_module', 'modules') checks the module key against the merged allowed-modules list of all groups the user belongs to (administrators always pass). Throwing AccessDeniedException produces a proper 403 response rather than a silent redirect, which makes verification unambiguous.
Granting access to a group
- In the Contao backend, go to System → User groups and edit the target group.
- Under Allowed modules, tick my_module (it appears under the section you registered it in, e.g. Content).
- Save. Members of the group see the menu entry after their next page load; other users do not.
Note the catch that trips people up: if a user belongs to no group with the module ticked, the module simply vanishes from their menu with no error message. When someone reports the module "missing", the group checkbox is the first thing to inspect.
Verification
- Log in as a backend user in the permitted group: the module appears in the navigation and its page renders.
- Log in as a user without the permission: the entry is absent from the menu, and requesting the module URL directly (e.g.
/contao?do=my_modulefor a BE_MOD module, or your controller route) returns HTTP 403. Check the status code in the browser's developer tools (Network tab) — a 200 response means your controller-level check is not running. - If the module does not appear even for administrators, the registration is not being loaded. Rebuild the cache with
vendor/bin/contao-console cache:clear(and warm up for the prod environment if applicable) and confirm yourcontao/config/config.phpor bundle class is actually part of the kernel.
Recovery and limitations
- Locked everyone out: administrators bypass module ACL, so log in as an admin and re-tick the group checkbox. No code change is needed to recover.
- Module visible but empty or erroring: that is a DCA/controller problem, not ACL — check the Contao log under
var/logs/for the real exception. - Granularity: the allowed-modules list is all-or-nothing per module. If you need finer control (e.g. per-record permissions), use Contao's permission voters or
$GLOBALS['TL_PERMISSIONS']with DCA-level checks instead — that is a separate mechanism from module visibility. - Version drift: the registration API (arrays vs. attributes, controller base classes) changed between Contao 4.9, 4.13 and 5.x. The ACL concepts (
BE_MOD, allowed modules,hasAccess()) are stable, but verify the exact class names against your installed version before deploying.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.