Search by

drago-ex / permission

accgit

Lightweight ACL and role management.

Package info

github.com/drago-ex/permission

pkg:composer/drago-ex/permission

Statistics

Installs: 95

Dependents: 2

Suggesters: 8

Stars: 0

Open Issues: 0

v1.0.0 2026-10-29 15:41 UTC

This package is auto-updated.

Last update: 2026-10-31 06:48:11 UTC


README

Lightweight ACL and role management. The package provides a central ACL factory, modular permission registration per module, and automatic authorization checks in presenters.

License: MIT PHP version Tests Coding Style

Requirements

  • PHP >= 8.3, < 9
  • Nette Framework
  • Composer

Installation

composer require drago-ex/permission

Features

  • Central ACL creation
  • Modular permission providers per module
  • Default roles: guest, user, admin
  • Automatic presenter authorization
  • Action and signal based privileges
  • Role switch panel for Tracy

Roles

Default roles:

  • guest
  • user (inherits from guest)
  • admin (inherits from user)

Roles are registered automatically.

Permission Factory

PermissionFactory creates a Nette\Security\Permission instance, registers default roles, and runs all registered permission providers.

Providers are collected via DI tags.

Permission Providers

Each module registers its own permissions using a Provider implementation.

Providers:

  • register ACL resources
  • define allow / deny rules
  • live inside the module they belong to

Typical resource naming:

  • Backend:Sign
  • Frontend:Article

Example provider for a Sign module:

use Drago\Permission\Provider;
use Drago\Permission\Role;
use Nette\Security\Permission;

final class PermissionProvider implements Provider
{
	private const string Resource = 'Backend:Sign';


	public function register(Permission $acl): void
	{
		$acl->addResource(self::Resource);
		$acl->allow(Role::RoleGuest, self::Resource);
	}
}

This registers the Backend:Sign resource and grants access to guests (unauthenticated users), which is the minimum required for the login page to be accessible.

Permission Generator (CLI)

The package provides a generator for module providers:

vendor/bin/create-permission

General usage:

vendor/bin/create-permission <ClassName> <Namespace> [Resource] [OutputDir] [options]

When you use the shared PermissionProvider class name, pass the resource and output directory explicitly. The class name is intentionally the same in every module and the namespace decides where the provider belongs.

Example for a Sign module:

vendor/bin/create-permission PermissionProvider App\Presentation\Sign Sign app/Presentation/Sign

Example for an Admin module:

vendor/bin/create-permission PermissionProvider App\Presentation\Backend\Admin Backend:Admin app/Presentation/Backend/Admin --allow-role=RoleAdmin --allow-with-resource=0

Options

  • --allow-role=<RoleConstant> default RoleGuest
  • --allow-privilege=<string> optional privilege argument for allow()
  • --add-resource=<0|1> default 1 (generate $acl->addResource(...))
  • --allow-with-resource=<0|1> default 1 (generate allow(..., self::Resource))
  • --allow=<rule> repeatable, custom allow rules
  • --force overwrite existing file

--allow formats:

  • --allow=RoleAdmin
  • --allow=RoleUser,self::Resource
  • --allow=RoleUser,self::Resource,default

Multi-rule example:

vendor/bin/create-permission PermissionProvider App\Presentation\Backend\Admin Backend:Admin app/Presentation/Backend/Admin --allow=RoleAdmin --allow=RoleUser,self::Resource,default

Generated register() example:

public function register(Permission $acl): void
{
	$acl->addResource(self::Resource);
	$acl->allow(Role::RoleAdmin);
	$acl->allow(Role::RoleUser, self::Resource, 'default');
}

Providers of Packages

A package that needs providers ships a script that calls create-permission with its arguments and lists it in composer.json; vendor/bin/drago-commands of drago-ex/project-tools runs the scripts of all installed packages:

"extra": {
	"drago-tools": {
		"commands": ["bin/create-front-permission"]
	}
}

DI Configuration

The package contains a ready configuration in vendor/drago-ex/permission/src/Permission/conf.neon. It is not loaded automatically; include it in your configuration, or copy its content:

includes:
	- ../vendor/drago-ex/permission/src/Permission/conf.neon

The search section needs nette/robot-loader, which this package requires. The bundled config contains:

  • permissionFactory service registration
  • automatic search registration for PermissionProvider classes in %appDir%/Presentation

Permission factory:

services:
	permissionFactory:
		class: Drago\Permission\PermissionFactory
		arguments: [tagged(PermissionTag)]

	- @permissionFactory::create

Module provider:

services:
	signPermission:
		class: App\Presentation\Sign\PermissionProvider
		tags: [PermissionTag]

For larger applications with many providers, you can use the search section to register all matching classes automatically instead of listing each one individually:

search:
	permissions:
		in: %appDir%/Presentation
		classes: [PermissionProvider]
		tags: [PermissionTag]

Presenter Authorization

Authorization is handled by the Authorization trait.

  • runs automatically on presenter startup
  • checks ACL using presenter name and resolved privilege

To activate authorization in a presenter, include the trait:

use Drago\Permission\Authorization;

class BasePresenter extends Nette\Application\UI\Presenter
{
	use Authorization;
}

All presenters extending BasePresenter will then have automatic authorization checks applied.

Unauthorized access:

  • not logged in: redirect to :Sign:in with a backlink (override signInDestination() to change the target); only a plain GET page is stored, a signal or a form is not replayed after signing in
  • logged in but forbidden: HTTP 403
  • a presenter without a registered ACL resource is denied the same way, so forgetting a provider never opens a page

Privilege resolution

The trait automatically resolves which ACL privilege to check based on the current request:

Situation Resolved privilege
Page load (no signal) {action}-read
Signal from a read-only receiver {component}-read
Signal listed as read-only {component}-read
Any other signal {component}-write
Direct presenter signal (no component) {signal}

For component signals, Nette returns a pair from Presenter::getSignal():

[$receiver, $signal] = $presenter->getSignal();

The receiver is the component path joined by hyphens. For example, if a presenter has a roles component and that component contains a dataGrid component with a paginator subcomponent, Nette can produce a signal like:

roles-dataGrid-paginator:page

This is resolved as:

$receiver = 'roles-dataGrid-paginator';
$signal = 'page';

The authorization trait uses the first receiver segment as the privilege group:

$group = explode('-', $receiver)[0]; // "roles"

So the signal above resolves to:

roles-read

when it is configured as read-only, otherwise it resolves to:

roles-write

Read-only signals

Override readOnlySignals() to declare signal names that should be treated as read operations. This checks only the signal name, not the component path.

protected function readOnlySignals(): array
{
	return ['sort', 'page', 'resetFilters', 'setPageSize'];
}

Examples:

Nette signal Receiver Signal Resolved privilege
roles-dataGrid:sort roles-dataGrid sort roles-read
roles-dataGrid-paginator:page roles-dataGrid-paginator page roles-read
roles-dataGrid:action roles-dataGrid action roles-write

Read-only receivers

Override readOnlyReceivers() to declare component path substrings whose signals should always resolve to read privilege. This checks the receiver path, not the signal name.

protected function readOnlyReceivers(): array
{
	return ['Grid-filters', 'Grid-paginator', 'Grid-pageSize'];
}

Any signal whose receiver name contains one of these strings will be resolved as {component}-read regardless of the signal name.

This is useful for nested controls where the signal name is generic, for example a filter form submit:

roles-dataGrid-filters-form:submit

The signal name is submit, but the receiver contains Grid-filters, so it resolves to:

roles-read

Be careful with broad receiver masks such as Grid. They make every signal from that receiver read-only, including row actions like:

roles-dataGrid:action

Use broad receiver masks only when the whole component is read-only.

Deciding in code

For more than name lists, override isReadOnlySignal(). It receives the receiver path and the signal name and replaces both lists. Drago DataGrid knows its own read-only signals (sorting, filters, paging, page size), so a presenter with grids can simply ask it:

use Drago\Datagrid\DataGrid;

protected function isReadOnlySignal(string $receiver, string $name): bool
{
	return DataGrid::isReadOnlySignal($this, $receiver, $name);
}

The grid looks the receiver up in the presenter: it does not depend on the name of the grid component, and a sort signal of another component stays a write. Row actions (roles-dataGrid:action) stay roles-write.

Full example

use Drago\Permission\Authorization;

class ArticlePresenter extends BasePresenter
{
	protected function readOnlySignals(): array
	{
		// these signals only read data checked as "{component}-read"
		return ['sort', 'page', 'resetFilters', 'setPageSize'];
	}

	protected function readOnlyReceivers(): array
	{
		// read-only DataGrid subcomponents with generic signal names
		return ['Grid-filters', 'Grid-paginator', 'Grid-pageSize'];
	}
}

With this configuration:

Nette signal Resolved privilege
roles-dataGrid:sort roles-read
roles-dataGrid-paginator:page roles-read
roles-dataGrid-filters:resetFilters roles-read
roles-dataGrid-filters-form:submit roles-read
roles-dataGrid-pageSize:setPageSize roles-read
roles-dataGrid:action roles-write

If you are not sure what Nette produces for a specific request, temporarily inspect:

bdump($this->getSignal());

or log the value inside resolveAclResource().

Write Buttons

The WriteAccess trait lets a component show its add, edit and delete buttons only to those who may use them. isWriteAllowed() asks for the same {component}-write privilege that the Authorization trait checks for the component's signals, so it works with static providers as well as with roles from the database:

use Drago\Permission\WriteAccess;

class ArticleControl extends Nette\Application\UI\Control
{
	use WriteAccess;
}
$grid->addActionEdit(fn(int $id) => $this->handleEdit($id))
	->setVisible($this->isWriteAllowed());

Hiding a button is only for comfort; the signal itself is still checked by the Authorization trait.

Role Switch in Tracy

While developing, the Tracy bar can switch the roles of the signed-in user, so you can try the application as another role without a test account. Register the panel (requires tracy/tracy):

tracy:
	bar:
		- Drago\Permission\Tracy\RoleSwitchPanel

The tab shows the current roles (red while they are switched). The panel lists the roles of the ACL, from static providers as well as from the database; My roles brings the real ones back, and signing out forgets the switch. Nette adds bar panels only in debug mode, so the panel never runs in production.

Optional add-ons

The package works without them. Set them up by hand as described below when the project needs them.

Related Package: Dynamic Role Management

For dynamic role and access management, use:

This package is built on top of drago-ex/permission and provides:

  • role creation
  • assigning roles to users
  • allowing or denying access per role