Authorization and Permissions

PagibleAI CMS authorization controls who can work with content and operational endpoints. A check combines the authenticated user, the active tenant, and registered actions stored in users.cmsperms.

This guide covers the admin panel, GraphQL, MCP, draft previews, and real-time channels. Visitor access to published pages and files is separate; configure it in Frontend Page Access Control.

Understand the CMS authorization boundary

Layer
Owner
What it decides
Authentication
Laravel Auth
Whether the request has a signed-in user
Tenant binding
Tenancy
Whether the user belongs to the current CMS tenant
CMS action
Permission and users.cmsperms
Whether the user may perform a registered action

Permission::can() applies all three checks in order. It returns false for guests, tenant mismatches, and unregistered or ungranted actions.

page:view means “view CMS records and preview drafts.” It is not a visitor-facing page permission. See Frontend Page Access Control for published pages and private files.

Manage users with cms:user

Create a user

Pass the email address. If the configured user model has no matching account, the command creates it and prompts for a password:

Prefer the prompt. A literal --password=... value can remain in shell history and process listings.

php artisan cms:user editor@example.com

Add and remove roles

--role adds one named role; it does not replace existing roles. Run one command per role. Remove a role by passing its name to --remove:

php artisan cms:user editor@example.com --role=editor
php artisan cms:user editor@example.com --role=publisher

php artisan cms:user editor@example.com --remove=editor

Add and remove permissions

The current CLI options are scalar, so use one command per permission or wildcard. Wildcards are expanded against the actions registered in that application:

php artisan cms:user editor@example.com --add=page:publish
php artisan cms:user editor@example.com --add=element:publish
php artisan cms:user editor@example.com --add="page:*"
php artisan cms:user editor@example.com --add="*:view"

php artisan cms:user editor@example.com --remove=page:purge
php artisan cms:user editor@example.com --remove="*:publish"

Inspect users and roles

php artisan cms:user editor@example.com --list
php artisan cms:user --roles

--enable stores every action registered at that moment as a concrete permission; it does not add the admin role, so later package permissions are not included automatically.

--disable removes registered concrete actions but does not remove named roles or stored wildcard entries. Remove each of those entries explicitly or use your application's account-status mechanism when you need a real login lock.

Option
Current behavior
--add= / -a
Add one permission, role name or wildcard pattern per CLI invocation
--remove= / -r
Remove one stored permission, role name or expanded wildcard per invocation
--role=
Add one configured role
--enable / -e
Add all actions currently registered as concrete entries
--disable / -d
Remove registered concrete actions; named roles and stored wildcards remain
--list / -l
Show configured roles and the resolved state of every registered action
--roles
List configured role definitions
--password= / -p
Set a password; avoid literal secrets in interactive shells
--quiet / -q
Suppress command output

Use the built-in roles

Built-in roles live in config/cms.php. Wildcards resolve against the action registry at runtime:

Role
Grants
Still excluded
admin
Every registered action
Nothing
viewer
page:view, element:view and file:view
All mutation actions
publisher
Registered page, element, file, audio, image and text actions
Actions outside those groups
editor
publisher minus every *:publish and *:purge action
Publishing and permanent deletion

Editors can still soft-delete with *:drop and restore soft-deleted records with *:keep. page:keep restores a dropped page; version restoration is handled by the page save workflow.

Run php artisan cms:user --roles against your installation because published configuration can override these defaults.

Understand registered actions

Core registers:

  • Pages: page:view, save, add, drop, keep, purge, publish, move and config
  • Elements: element:view, save, add, drop, keep, purge and publish
  • Files: file:view, save, add, drop, keep, purge and publish

Installed packages extend this list. The AI package registers chat, refinement, description, transcription, image and text actions. GraphQL adds cache:clear and page:metrics; Pulse adds pulse:view.

Permission::all() is the authoritative runtime list. A wildcard never grants an unregistered action.

Define custom roles

Syntax
Example
Meaning
Permission
page:view
Grant one registered action
Resource wildcard
page:*
Grant registered actions in one resource group
Operation wildcard
*:view
Grant the operation across registered groups
All actions
*
Grant every registered action
Denial
!page:purge
Subtract one action
Wildcard denial
!*:publish
Subtract an operation across groups
Role reference
editor
Expand another configured role
// config/cms.php
'roles' => [
    'admin' => ['*'],
    'reviewer' => [
        'page:view',
        'element:view',
        'file:view',
    ],
    'media-manager' => [
        'file:*',
        'image:*',
        '!file:purge',
    ],
    'senior-editor' => [
        'editor',
        'page:publish',
        'element:publish',
    ],
],

PagibleAI recursively expands role references and wildcards, then subtracts denials. Keep role references acyclic.

Check and change permissions in PHP

Permission::can() returns false unless the user is authenticated, belongs to the current tenant and the action is registered. Use the concrete action you are authorizing; Permission::can('*', $user) only reports whether the user has any stored CMS permission entry.

use Aimeos\Cms\Permission;

if (!Permission::can('page:publish', $user)) {
    abort(403);
}

$states = Permission::get($user);
$actions = Permission::all();
$roles = Permission::roles();
$editor = Permission::role('editor');

$user = Permission::add(
    ['page:publish', 'element:publish'],
    $user
);
$user->save();

$user = Permission::remove('page:purge', $user);
$user->save();

Assign cmsperms directly

The configured Eloquent user model receives an array cast for cmsperms. Direct assignment is useful during imports or account creation, but it does not invalidate the permission cache for an already-checked user object. Prefer Permission::add() and remove() during a request.

$user->cmsperms = [
    'publisher',
    '!*:purge',
];
$user->save();

Register custom actions

Register package actions during service-provider boot before roles or callbacks try to use them. Unregister actions when a conditional integration is disabled:

use Aimeos\Cms\Permission;

Permission::register([
    'seo:analyze',
    'seo:submit',
]);

Permission::unregister('seo:submit');

Integrate external CMS authorization

Configure callbacks once in a service provider. The check callback runs only after authentication, tenant membership and action registration have succeeded. Grant and revoke callbacks receive either one action string or an array.

use Aimeos\Cms\Permission;
use Illuminate\Contracts\Auth\Authenticatable;

Permission::canUsing(
    function (string $action, Authenticatable $user): bool {
        return ExternalRbac::allows($user->getAuthIdentifier(), $action);
    }
);

Permission::addUsing(
    function (array|string $actions, Authenticatable $user): Authenticatable {
        ExternalRbac::grant($user->getAuthIdentifier(), (array) $actions);
        return $user;
    }
);

Permission::removeUsing(
    function (array|string $actions, Authenticatable $user): Authenticatable {
        ExternalRbac::revoke($user->getAuthIdentifier(), (array) $actions);
        return $user;
    }
);

Pass null to a *Using() method to restore its default behavior, especially when isolating tests.

Bind permission checks to the tenant

Without tenancy configuration, any authenticated user belongs to the single empty tenant and then proceeds to the permission check.

With tenancy enabled, the default rule already requires user.tenant_id to equal Tenancy::value(). Tenancy::stancl() connects this shared-database CMS context to stancl lifecycle events. Per-database tenancy is not supported.

use Aimeos\Cms\Tenancy;
use App\Auth\TenantMemberships;
use Illuminate\Contracts\Auth\Authenticatable;

Tenancy::stancl();

// Optional: replace the default tenant_id equality check.
Tenancy::$access = static function (
    ?Authenticatable $user,
    string $tenant
): bool {
    return $user !== null
        && TenantMemberships::allows($user, $tenant);
};

Omit the Tenancy::$access override when the default tenant_id equality rule matches your application. When you do override it, call your real membership service; the callback is a tenant-membership check, not a replacement for Permission::can().

See Multi-Tenancy SaaS Setup for tenant initialization and shared-database setup.

Authorize GraphQL fields

Use Lighthouse @guard to require authentication and @cmsPermission to require CMS actions. Multiple actions require all permissions by default; set any: true when one successful check is enough.

extend type Mutation {
  saveReview(id: ID!): Page
    @guard
    @cmsPermission(action: ["page:view", "page:save"])

  inspectContent(id: ID!): Page
    @guard
    @cmsPermission(
      action: ["page:view", "element:view"]
      any: true
    )
}

A denied field returns Insufficient permissions. The shipped schema also protects sensitive nested relationships, so keep the directive on new relationship fields rather than guarding only top-level queries.

Admin controls hidden by the Vue client are a usability feature. Server-side GraphQL, controllers and resources remain the authorization boundary.

Authorize real-time channels

When broadcasting is enabled, page, element and file channels require the matching *:view permission. Multi-tenant channel authorization also requires the channel tenant to equal the request tenant. Ensure the broadcasting-auth route runs the middleware that initializes tenancy.

Authorization checklist

  • Register every CMS action before assigning it.
  • Store roles and direct actions in cmsperms; persist changes.
  • Initialize tenancy before checking permissions.
  • Protect GraphQL fields and nested relations server-side.

Troubleshoot authorization

Permission::can() unexpectedly returns false

Confirm the action is registered, the user is authenticated, the current tenant is initialized and the user has a matching role or action.

A role change is not visible

Use Permission::add() or remove(), save the returned user and refetch long-lived user objects.

GraphQL denies a multi-action field

Check whether the field requires all actions or explicitly uses any: true.

Broadcast authentication returns 403

Verify tenant middleware, the channel tenant and the matching page, element or file view permission.