Permissions
This document explains how the package decides which panel users may perform which actions inside the admin panel. It covers the cms.permissions config group, how to grant abilities through Laravel's Gate, the complete list of abilities the package checks, and what the permission system does not cover. It is written for the developer who configures access for a site's editors.
The package defines a set of named abilities and asks Laravel's Gate whether the signed-in user has each one. It does not ship roles, a permissions table, or a dependency on a permissions package. An application that already uses spatie/laravel-permission, a custom is_editor column, or no permission system at all can answer the package's questions with what it already has, and does not need to adopt a second system.
Panel access itself (whether a user can open the panel at all) is a separate check, made by canAccessPanel() on the User model. It is described in Installation.
Enabling permissions
The permission system is off by default:
'permissions' => [
'enabled' => false,
'super_admin' => null,
],
With enabled set to false, the panel behaves as it did before the permission system existed. That is the default so that upgrading the package never removes an existing user's access to their own site.
Enabling permissions without losing access
Grant a super-admin ability first, then define narrower abilities. The two code blocks below show steps 1 and 2 in order.
- In
config/cms.php, enable permissions and name a super-admin ability. - In
app/Providers/AppServiceProvider.php, define that ability in theboot()method. - In the same
boot()method, define the specific abilities for other users, as shown under Granting abilities.
'permissions' => ['enabled' => true, 'super_admin' => 'cms.admin'],
// AppServiceProvider::boot()
Gate::define('cms.admin', fn (User $user) => $user->is_cms_admin);
Abilities::allows() (src/Support/Abilities.php) checks the super_admin ability before the specific ability. A user who has the super-admin ability is granted every ability, whatever the specific ability's definition returns.
The user is resolved from the panel's guard (Filament::auth()), falling back to the default guard. When no user is signed in, every check is refused.
Granting abilities
Abilities are ordinary Gate abilities. Define them anywhere Laravel's Gate reads definitions: a service provider, a policy, or the wildcard bindings of spatie/laravel-permission.
use Illuminate\Support\Facades\Gate;
use Mcg\FilamentCms\Support\Abilities;
Gate::define(Abilities::CONTENT_VIEW, fn (User $user) => $user->isStaff());
Gate::define(Abilities::CONTENT_UPDATE, fn (User $user) => $user->isStaff());
Gate::define(Abilities::CONTENT_PUBLISH, fn (User $user) => $user->isEditor());
Gate::define(Abilities::SUBMISSIONS_VIEW, fn (User $user) => $user->isAdmin());
With spatie/laravel-permission, each ability name becomes a permission of the same name:
foreach (Abilities::all() as $ability) {
Permission::findOrCreate($ability);
}
Abilities::all() returns every ability the package checks, read from the constants of the Abilities class. The same list is the input for a role editor.
The abilities
Each ability is a constant on Mcg\FilamentCms\Support\Abilities.
Content
| Ability | Constant | Covers |
|---|---|---|
| cms.content.view | CONTENT_VIEW | Seeing the content section and opening a page |
| cms.content.create | CONTENT_CREATE | Creating pages, snippets and custom content types |
| cms.content.update | CONTENT_UPDATE | Saving a draft |
| cms.content.delete | CONTENT_DELETE | Deleting |
| cms.content.publish | CONTENT_PUBLISH | Publishing and unpublishing |
| cms.content.restore-version | CONTENT_RESTORE_VERSION | Rolling a page back to an earlier version |
Publishing is a separate ability from editing. Writing a draft and deciding that the public site shows it are often different responsibilities, held by different people. A user with CONTENT_UPDATE but not CONTENT_PUBLISH gets a working editor with no Publish button.
Scoping content abilities by content type
A content ability receives the Content record when one exists, and the content type key (a string such as page) where there is no record. One ability definition can therefore treat each content type differently, without a separate ability per type:
Gate::define(Abilities::CONTENT_UPDATE, function (User $user, Content|string $subject) {
$type = $subject instanceof Content ? $subject->type : $subject;
return $type === 'page' || $user->isAdmin();
});
Media, menus, tags, redirects and forms
Each of these five sections has four abilities: view, create, update and delete.
| Section | Abilities |
|---|---|
| Media | cms.media.view, cms.media.create, cms.media.update, cms.media.delete |
| Menus | cms.menus.view, cms.menus.create, cms.menus.update, cms.menus.delete |
| Tags | cms.tags.view, cms.tags.create, cms.tags.update, cms.tags.delete |
| Redirects | cms.redirects.view, cms.redirects.create, cms.redirects.update, cms.redirects.delete |
| Forms | cms.forms.view, cms.forms.create, cms.forms.update, cms.forms.delete |
The constants follow the pattern MEDIA_VIEW, MENUS_CREATE, TAGS_UPDATE, REDIRECTS_DELETE, FORMS_VIEW. Denying a section's view ability removes that section from the panel navigation.
Form submissions
| Ability | Constant | Covers |
|---|---|---|
| cms.submissions.view | SUBMISSIONS_VIEW | Seeing the submissions inbox at all |
| cms.submissions.update | SUBMISSIONS_UPDATE | Changing a submission's status and internal notes |
| cms.submissions.delete | SUBMISSIONS_DELETE | Deleting submissions, one at a time or in bulk |
| cms.submissions.export | SUBMISSIONS_EXPORT | Downloading submissions as a CSV file |
The submission abilities are separate from the form abilities. Building a contact form is a content task. Reading what people sent through it means reading their names, addresses and attachments, and the two tasks are often assigned to different people. Denying SUBMISSIONS_VIEW hides the whole inbox rather than disabling its buttons.
Export has its own ability because reading one submission on screen and downloading every submission as a file give a user different amounts of personal data.
Site settings
| Ability | Constant | Covers |
|---|---|---|
| cms.settings.view | SETTINGS_VIEW | Opening the Site settings page |
| cms.settings.update | SETTINGS_UPDATE | Saving the page, and the Send a test message action in its Notifications section |
SETTINGS_UPDATE is checked inside SiteSettingsPage's save method and test method, not only on the buttons that call them. A Livewire method can be called by anyone who can load the component, whether or not the interface shows a button for it, so a check on the button alone would not prevent the save.
API tokens
| Ability | Constant | Covers |
|---|---|---|
| cms.tokens.view-all | TOKENS_VIEW_ALL | Seeing and revoking API tokens that belong to other users on the API Tokens screen |
A user always sees their own tokens; this ability adds every other user's tokens to the list, with the owner's name. The list shows each token's label, owner and last-used time. Sanctum stores only a hash of each token, so no token value is shown. The panel offers no way to create a token for another user, because a token carries its holder's authority and creating one for someone else would grant that authority to the creator.
The raw HTML block
cms.blocks.raw_html_ability is a separate setting, older than the permission system. It names a Gate ability that a user must have to place the html block, whose output is rendered without escaping. It applies whether permissions.enabled is true or false. See Blocks.
What permissions do not cover
Two further areas are outside the permission system:
- The public site. These abilities apply to the panel only. Published content is public. There is no per-page audience control, and enabling permissions does not add one.
- Ownership. The package has no ownership model. A rule such as "editors may change only their own pages" requires a Gate definition that inspects the record. The
created_bycolumn on each version supports this:
Gate::define(Abilities::CONTENT_UPDATE, fn (User $user, Content|string $subject) =>
$user->isAdmin() || (
$subject instanceof Content
&& $subject->draftVersion?->created_by === $user->id
));