Access Control plugin screenshot
Dark mode ready
Multilingual support
Supports v5.x

Access Control

Community

Filament UI for happenv-com/laravel-access-control: a roles × permissions matrix and a permission editor for a single role or user.

Tags: Panel Authorization
Supported versions:
5.x 4.x
Happenv sp. z o.o. avatar Author: Happenv sp. z o.o.

Package health

Beta

Automated checks of this plugin's Composer package

100 / 100
Security 100
Maintenance 100
Ecosystem 100
15 checks
  • Passed: GitHub Actions pinned to SHA
  • Skipped: GitLab CI includes pinned to SHA
  • Passed: Open security advisories
  • Passed: Dependabot PR responsiveness — No open Dependabot PRs.
  • Skipped: Renovate MR responsiveness
  • Passed: Dependabot or Renovate configured
  • Passed: Dependency update cooldown configured
  • Passed: Provides a security policy
  • Passed: Abandoned or archived — No consulted source marks the package abandoned (packagist, github).
  • Passed: Commit and release recency — Active: last commit 0 days ago; last release 0 days ago.
  • Passed: composer.lock not committed by library — composer.lock is absent from the released dist archive.
  • Passed: Dist archive is lean
  • Passed: Current Laravel version supported — Package dependencies resolve together with current Laravel 13.0.
  • Passed: Current PHP version supported — Constraint ^8.3 supports current PHP 8.5.
  • Skipped: Current Symfony version supported
Third-party plugin. This is built by the community, not the Filament team. Filament does not review, endorse, or vet the security of plugins outside the filament/ namespace. Review the source and install at your own risk. Found malware or an unresolved security issue the author won't address? Report it .
Powered by Plumb Last scanned 8 hours ago

Documentation

Filament Access Control

Latest Version Tests PHPStan Quality Total Downloads License

The Filament screens for happenv-com/laravel-access-control: every role against every permission in one grid, and the permissions of a single role or user wherever you want them — a section of the edit form, a tab, a page of their own. Changes are written the moment they are clicked, or collected until the operator presses Save permissions.

use Happenv\FilamentAccessControl\FilamentAccessControlPlugin;
use Happenv\FilamentAccessControl\Schemas\Components\PermissionEditor;

$panel->plugin(
    FilamentAccessControlPlugin::make()
        ->roleModel(Role::class)
        ->superAdminRole('administrator'),
);

// In the role's (or the user's) form:
PermissionEditor::make()->deferred();

#Key features

  • A roles × permissions matrix. One Filament table: modules as collapsible groups, a row per subject and per verb, a column per role — with an optional "granted/total" per role right beside each group's name, folded or open. With dozens of roles, pick the ones shown in the role picker, while the permission column and the role header stay in view. See The access control page, Many roles and Counters.
  • An editor for one role or one user. The same table for a single record, as a schema component in a form, a tab or an infolist; for a user it also lists the roles that already grant each permission. PermissionSelector covers create forms as a plain form field. See Editing one record and The form field.
  • Live or deferred saving. Every click written at once, or staged and saved together with Discard and a warning before leaving; each save replays the operator's intent under a row lock, so two administrators do not overwrite each other. See Live or deferred.
  • Why, not just whether. Every cell shows what laravel-access-control resolves: in effect, implied, missing a requirement, blocked by a conflict, restricted, or withheld by a condition — the tooltip names the permissions involved. See Rules and conditions.
  • #[RequiresMFA]. Withhold a permission from any account without multi-factor authentication; the user editor says what it withholds. See Rules and conditions.
  • Your authorization, asked every time. Laravel abilities, policies or access-control permission enums decide who may see, create and change roles; a voter's refusal is shown in the operator's language. See Authorization.
  • Surfaces. Narrow a screen to what a surface offers (an API key's screen, say); grants held outside it stay listed and revocable. See Surfaces.
  • Tested. Covered by a Pest suite on every supported version combination.

#Requirements

Package Versions
PHP 8.3 – 8.5
Laravel 12, 13
Filament 4, 5
happenv-com/laravel-access-control 3.1+

#Installation

Install the package via Composer:

composer require happenv-com/filament-access-control

[!IMPORTANT] If you have not set up a custom theme and are using Filament Panels, follow the instructions in the Filament docs first.

Add the package's views to your theme's CSS file, so Tailwind generates the classes they use:

@source '../../../../vendor/happenv-com/filament-access-control/resources/**/*.blade.php';

The matrix brings a small stylesheet of its own (see Many roles). php artisan filament:assets publishes it — Filament's filament:upgrade, which a Filament application runs after every composer update, does that already.

#Preparing your models

A record whose permissions the screens edit — a role, or a user holding permissions directly — implements HasEditablePermissions: the same two methods laravel-access-control's HasPermissions trait asks for, made public. setPermissions() must persist.

use Happenv\FilamentAccessControl\Contracts\HasEditablePermissions;
use Happenv\LaravelAccessControl\Contracts\AuthControllable;
use Happenv\LaravelAccessControl\Traits\HasPermissions;
use Illuminate\Support\Collection;

class Role extends Model implements AuthControllable, HasEditablePermissions
{
    use HasPermissions;

    protected $casts = ['permissions' => 'array'];

    public function getPermissions(): Collection
    {
        return collect($this->permissions ?? [])->filter(fn ($slug) => is_string($slug))->values();
    }

    public function setPermissions(Collection $permissions): void
    {
        $this->permissions = $permissions->values()->all();
        $this->save();
    }
}

A user edited the same way can also expose getRoles(): iterable (as HasRoles does) — the editor then shows which of the user's roles already grant each permission.

#Registering the plugin

use Happenv\FilamentAccessControl\FilamentAccessControlPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        ->plugin(
            FilamentAccessControlPlugin::make()
                ->roleModel(Role::class)
                ->superAdminRole('administrator') // the `code` of the role that holds everything
                ->roleAbilities(
                    viewAny: RolePermission::View,
                    create: RolePermission::Create,
                    update: RolePermission::Update,
                ),
        );
}

#Configuration

The package has no config file: everything is set on the plugin — see Registering the plugin and The access control page — or per component.

Optionally, publish the views and translations:

php artisan vendor:publish --tag="filament-access-control-views"
php artisan vendor:publish --tag="filament-access-control-translations"

#Usage

#The access control page

The Access control page: a column per role, a Work group open with its summary row, Dependencies badges and a tooltip naming the conflict that blocks a cell

With a role model, the plugin registers an Access control page: the matrix and an Add role action. Roles are deleted where your application manages them — its role resource, for instance. Configure it through the plugin:

FilamentAccessControlPlugin::make()
    ->roleModel(Role::class)
    ->roleTitleAttribute('name')                          // or fn (Role $role): string
    ->modifyRolesQueryUsing(fn (Builder $query) => $query->orderBy('name'))
    ->superAdminRole(fn (Role $role): bool => $role->is_admin)
    ->rolesShownByDefault(8)                               // see "Many roles" below
    ->modifyCreateRoleActionUsing(fn (CreateAction $action) => $action->schema([
        TextInput::make('name')->required(),
        TextInput::make('code')->required()->unique(),
    ]))
    ->navigationGroup('Settings')
    ->navigationSort(10)
    ->slug('permissions')
    ->cluster(SettingsCluster::class);

Groups start folded, and a group's row opens and folds it. Only open groups are drawn, so a catalogue of hundreds of permissions stays a light page; Expand all and a search open what they show.

The super-admin role is drawn fully granted and read-only. Pass ->accessControlPage(false) to register no page, or ->accessControlPage(MyPage::class) with a class extending Pages\AccessControl to replace it.

#Many roles

The list icon next to the search opens the role picker: every role with a checkbox, a search, and Select all / Deselect all. The choice is kept for the session, and while roles are hidden the table says how many it shows (Roles shown: 8 of 50). A role added from the page is shown straight away.

FilamentAccessControlPlugin::make()
    ->rolesShownByDefault(8)     // the first eight roles, in the roles query's order, until the operator picks others
    ->deferRolePicker(false);    // apply every tick at once instead of on Apply

PermissionMatrix::make()->rolesShownByDefault(8)->deferRolePicker(false);   // or per component

Unset, every role shows and the picker waits for Apply. Fewer columns also make a lighter page: each column is a cell in every row, and each click redraws the table. The picker is built from Filament's own table filters — the trigger, the modal, Apply and Reset — and narrows the columns, never the rows.

However many are shown, the matrix keeps its bearings as it scrolls: the permission column stays at the start while the roles scroll past it, and the row of role names stays at the top while the permissions scroll under it. Filament's table has no sticky column or header, so this is the package's one stylesheet — plain CSS on Filament's classes, confined to the matrix.

To put the matrix somewhere else — a page of your own, a tab of a resource — use the schema component:

use Happenv\FilamentAccessControl\Schemas\Components\PermissionMatrix;

PermissionMatrix::make()->deferred();

or the Livewire component directly: @livewire(\Happenv\FilamentAccessControl\Livewire\RolePermissionMatrix::class, ['deferred' => true]).

#Editing one record

A user's permission editor in deferred mode: Granted, From roles and In effect columns, a staged grant awaiting Save permissions, and a callout naming the permission withheld until the account enables MFA

PermissionEditor edits the permissions of the schema's record. Put it wherever the schema allows:

use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Happenv\FilamentAccessControl\Schemas\Components\PermissionEditor;

public static function configure(Schema $schema): Schema
{
    return $schema->components([
        Tabs::make()->tabs([
            Tab::make('Role')->schema([
                TextInput::make('name')->required(),
            ]),
            Tab::make('Permissions')->schema([
                PermissionEditor::make(),
            ]),
        ]),
    ]);
}

The editor saves on its own, independently of the form around it — the form's Save changes never touches the permissions. It is hidden while the record does not exist yet (a create page), and read-only in a disabled schema (a view page) or when ->disabled().

For a user, the From roles column lists the roles that already grant each permission, and a super-admin role is called out above the table. Hide the column with ->showInheritedPermissions(false).

Edit a record other than the schema's own through the component's data: PermissionEditor::make()->data(fn (User $record) => ['record' => $record->apiKey]).

#Live or deferred

By default every click is written at once. Deferred screens stage the clicks instead — changed cells turn amber — and write them together with Save permissions, or throw them away with Discard:

FilamentAccessControlPlugin::make()->deferred();   // the default for every screen of the plugin

PermissionEditor::make()->deferred();               // or per component
PermissionEditor::make()->deferred(false);

Leaving a page with staged changes asks for confirmation first.

#Counters

Each group's own row can show what every role holds of it — one "granted/total" number per role column (3/7), beside the group's name, so a folded group still tells whether it is worth opening; a subject's tooltip then counts its verbs too. Off by default:

FilamentAccessControlPlugin::make()->counters();   // every screen of the plugin

PermissionEditor::make()->counters();              // or per component
PermissionMatrix::make()->counters(false);

#Authorization

Every change asks the gate, as the panel's user, with the record being changed:

Screen Asks Default
Access control page roleAbilities(viewAny:) with the role model class viewAny
Changing a role roleAbilities(update:) with the role update
Add role roleAbilities(create:) with the role model class create
PermissionEditor (a user) ->ability(...) with the record update

An ability can be a Laravel ability name (a policy method as often as not), a laravel-access-control permission enum — asked with the record only, as voters expect — or a closure receiving record, model and user. null switches the check off. When a voter refuses, its own message reaches the operator; the library's generic Unauthorized for <slug> is translated into the permission's name.

#Surfaces

laravel-access-control lets a permission declare the surfaces it is available on with #[AvailableFor]. Narrow a screen to one:

PermissionEditor::make()->surface(PermissionSurface::Api);

Only what the surface offers can be granted there; what the record already holds outside of it is listed in a group of its own — revocable, never grantable again. A surface enum that implements OffersEveryPermission and returns true offers the whole catalogue.

#Rules and conditions

laravel-access-control 3 lets permissions depend on each other (#[Requires], #[ImpliedBy], #[ConflictsWith]) and on the account (conditions). The screens show all of it; they never decide anything themselves.

Cells. A role's cell shows what the rules make of the role's grants; a user's In effect column what its roles, the rules, runtime restrictions and its conditions leave it:

Icon Colour Means
check-circle success stored and in effect
check-circle info in effect, implied by another permission (a click grants it explicitly)
exclamation-triangle warning granted, but a permission it requires is not in effect
no-symbol danger granted, but blocked by a permission it conflicts with
lock-closed gray granted, but the application restricts it right now
shield-exclamation warning granted, but the account does not meet a condition
x-circle danger not granted

The In effect column shows only where it can differ from Granted: the account holds a role, a permission of the screen takes part in a rule or carries a condition, or the application restricts one right now. An API key holding nothing but direct grants, in a catalogue without rules, gets no column that would repeat Granted.

The tooltip names the permissions involved. In deferred mode a changed cell takes the primary colour, and every other cell already shows the consequence of the change.

The user editor counts a super-admin role as holding every permission with its conditions still applied — an unmet #[RequiresMFA] still shows. But an application that implements its super-admin through Gate::before() skips conditions at the gate along with everything else, so there the column overstates what is actually enforced.

Dependencies. A column next to the permission's name lists every rule from that permission's side — Requires / Required by, Implied by / Implies, Blocked by / Blocks — and every condition. The rule's reason is its tooltip. Searching also finds the permissions a rule ties to what you typed.

Conditions — #[RequiresMFA]. Put it on a permission enum or case to withhold the permission from any account without multi-factor authentication enabled on the panel:

use Happenv\FilamentAccessControl\Attributes\RequiresMFA;
use Happenv\LaravelAccessControl\Contracts\PermissionDefinition;

enum OrderPermission: string implements PermissionDefinition
{
    #[RequiresMFA]
    case Refund = 'order.refund';

    // The providers of a named panel, rather than the current one:
    #[RequiresMFA(panel: 'admin')]
    case Export = 'order.export';
}

It fails closed: an account without MFA, an account the panel's providers cannot ask (an API key) and a panel without multi-factor authentication do not meet it. The user editor says above the table how many permissions a condition withholds. Your own conditions are attributes implementing laravel-access-control's PermissionCondition — see its README; implement DescribesPermissionCondition to name them on these screens. A Gate::before() that answers first skips conditions like any other gate check.

Declaration problems. A permission declared so that it can never be allowed (it requires what it conflicts with), or a rule pointing at an enum nobody registered, is listed above the screens and marked Invalid declaration. ->declarationProblems(false) hides both.

For the In effect column to tell implied permissions from stored ones, roles using HasPermissions should implement laravel-access-control's HoldsGrants.

#The form field

PermissionSelector is a form field holding the slugs as a flat list, saved with the form like any other field — for create forms, or anything that must save in one go:

use Happenv\FilamentAccessControl\Forms\Components\PermissionSelector;

PermissionSelector::make('permissions')->surface(PermissionSurface::Api);

It validates what arrives, keeps grants the deployment cannot draw (a module left out of the build), and never lets a slug outside the surface in.

#Naming verbs

A permission row shows its verb — View, Update — translated from filament-access-control::permissions.actions.<case_name_in_snake_case>, or the case name when there is no translation. Name your own verbs with a resolver, or by publishing the translations:

use Happenv\FilamentAccessControl\Support\PermissionTree;

PermissionTree::resolveActionLabelsUsing(
    fn (PermissionDto $permission): ?string => __("app.permission-verbs.{$permission->enum->name}"),
);

#Reacting to changes

Every write dispatches Happenv\FilamentAccessControl\Events\PermissionsUpdated with the record and what was actually granted and revoked — for an audit log, a cache to clear.

#Translations

The package ships in every locale Filament ships:

am ar az bg bn bs ca ckb cs da de el en es et eu fa fi fil fr he hi hr hu hy id it ja ka km ko ku lt lus lv mk mn ms my nb ne nl pl pt pt_BR ro ru sk sl sq sr_Cyrl sr_Latn sv sw tg th tr uk ur uz vi zh_CN zh_HK zh_TW

The test suite keeps it that way: a locale Filament adds and this package lacks fails it, and so does a key missing from any locale.

Publish them to change the wording:

php artisan vendor:publish --tag="filament-access-control-translations"

#Development

composer test          # unit and feature tests
composer phpstan       # static analysis
composer cs            # fix code style: composer normalize, Rector, Pint
composer ci            # everything CI checks, locally

#Upgrading

Breaking changes and how to migrate are described in UPGRADING for every major version.

#Changelog

See CHANGELOG and GitHub releases for what has changed recently.

#Contributing

See CONTRIBUTING for details.

#Security vulnerabilities

Please review our security policy on how to report security vulnerabilities.

#Credits

#License

The MIT License (MIT). See License File for more information.


Happenv

The author

Happenv sp. z o.o. avatar Author: Happenv sp. z o.o.

Happenv is a software development company specializing in e-commerce solutions, logistics systems, and Order Management Systems (OMS). We design, build, and maintain scalable business applications that help companies streamline operations, automate workflows, and improve customer experiences. Our expertise includes custom development, system integrations, and long-term support of solutions built with Laravel and Filament, delivering reliable and efficient platforms tailored to modern commerce and logistics needs.

Plugins
10
Stars
2

From the same author