Authorisation
Textstem's authorisation layer is built on spatie/laravel-permission and Laravel's Gate/Policy system. This document covers how the default system works and how to extend it with custom per-instance rules and admin list filtering.
How It Works
There are three levels of authorisation:
- Super-admin bypass -- users with the
super-adminrole pass all gates unconditionally. - Role-based permissions -- standard Spatie permissions checked in each policy method.
- Instance-level access -- optional custom classes that restrict which specific model records a user can act on.
Super Admin
Any user assigned the super-admin role bypasses all gate checks. This is wired via Gate::before in the service provider:
Gate::before(function ($user, $ability) {
if ($user->isSuperAdmin()) {
return true;
}
});
isSuperAdmin() is a method on the User model:
public function isSuperAdmin(): bool
{
return $this->hasRole('super-admin');
}
Super-admins can also impersonate other users (except other super-admins).
Built-in Policies
Textstem ships policies for all core models. They are auto-resolved from the model class name -- no manual registration is needed:
| Model | Policy |
|---|---|
WranglerPage |
WranglerPagePolicy |
WranglerComponent |
WranglerComponentPolicy |
Post |
PostPolicy |
Asset |
AssetPolicy |
Category |
CategoryPolicy |
Collection |
CollectionPolicy |
Event |
EventPolicy |
Tag |
TagPolicy |
User |
UserPolicy |
Policy Methods
Each policy implements the standard set of Laravel policy methods. The permission name used is derived from the action and the lowercase model name (plural or without spaces):
| Method | Checks permission | Also checks instance access |
|---|---|---|
viewAny |
list {model} |
No |
view |
view {model} |
No |
create |
create {model} |
No |
update |
update {model} |
Yes (where supported) |
delete |
delete {model} |
Yes (where supported) |
deleteAny |
delete {model} |
No |
restore |
-- | Always false |
forceDelete |
-- | Always false |
Permission names by model
| Model | Permission names |
|---|---|
WranglerPage |
list wranglerpages, view wranglerpages, create wranglerpages, update wranglerpages, delete wranglerpages |
WranglerComponent |
list wranglercomponents, view wranglercomponents, create wranglercomponents, update wranglercomponents, delete wranglercomponents |
Post |
list posts, view posts, create posts, update posts, delete posts |
Asset |
list assets, view assets, create assets, update assets, delete assets |
Category |
list categories, view categories, create categories, update categories, delete categories |
Customising Authorisation
The built-in policies check permissions, but do not restrict access to individual model instances by default. For finer-grained control -- for example, limiting an editor to only pages in a specific category or URL prefix -- you can create a custom auth class in your host application.
Instance-Level Access
Create a class in App\Domain\Admin\Auth\ named after the model. The class must implement an allowed() method that receives the user, the model instance, and the action string, and returns a boolean.
For WranglerPage:
<?php
namespace App\Domain\Admin\Auth;
use Medialight\Textstem\Models\User;
use Medialight\Textstem\Models\WranglerPage as WranglerPageModel;
class WranglerPage
{
public function allowed(User $user, WranglerPageModel $model, string $action = ''): bool
{
// Return true if this user can perform $action on this specific page.
// Example: restrict to pages in a category the user is responsible for.
return $user->hasRole('super-admin') || $model->category_id === $user->category_id;
}
}
For WranglerComponent:
<?php
namespace App\Domain\Admin\Auth;
use Medialight\Textstem\Models\User;
use Medialight\Textstem\Models\WranglerComponent as WranglerComponentModel;
class WranglerComponent
{
public function allowed(User $user, WranglerComponentModel $model, string $action = ''): bool
{
return true; // add your logic here
}
}
When the policy runs checkModelAccess(), it detects your class and delegates to allowed(). If the class does not exist, all model instances are accessible to any user who holds the required permission.
The $action parameter will be the ability being checked (e.g. 'update', 'delete'), giving you the ability to differentiate between actions if needed.
Filtering Admin Lists
To control which records appear in admin listing views, add a scopes() method to the same auth class. The method receives the Eloquent query builder by reference and optionally the current user.
<?php
namespace App\Domain\Admin\Auth;
use Illuminate\Database\Eloquent\Builder;
use Medialight\Textstem\Models\User;
use Medialight\Textstem\Models\WranglerPage as WranglerPageModel;
class WranglerPage
{
public function allowed(User $user, WranglerPageModel $model, string $action = ''): bool
{
// ...
}
public function scopes(Builder &$query, ?User $user = null): void
{
if (! $user) {
$user = auth()->user();
}
// Example: limit the list to pages under the user's regional URL prefix
$region = $user->region; // your own attribute
$query->where('url', 'LIKE', "/{$region}%");
}
}
Supported Models for Custom Auth Classes
Custom auth classes are supported for the following models. The class name must exactly match the model base name:
| Class to create | Applies to |
|---|---|
App\Domain\Admin\Auth\WranglerPage |
WranglerPage update and delete |
App\Domain\Admin\Auth\WranglerComponent |
WranglerComponent update and delete |
Other policies (Post, Asset, Category, etc.) perform only permission checks and do not call checkModelAccess. To customise access for those models, extend the policy directly (see below).
Extending a Policy
If you need to override a full policy -- for example to add instance-level checks to PostPolicy -- extend the package class and bind it in a service provider in your host application:
<?php
namespace App\Policies;
use Medialight\Textstem\Models\Post;
use Medialight\Textstem\Models\User;
use Medialight\Textstem\Policies\PostPolicy as BasePostPolicy;
class PostPolicy extends BasePostPolicy
{
public function update(User $user, Post $model): bool
{
return parent::update($user, $model) && $model->author === $user->name;
}
}
Register it in AppServiceProvider::boot():
use App\Policies\PostPolicy;
use Medialight\Textstem\Models\Post;
Gate::policy(Post::class, PostPolicy::class);
Checking Authorisation in Your Code
Standard Laravel patterns apply throughout:
// In a controller or Livewire component
$this->authorize('update', $page);
// In a Blade view
@can('create', \Medialight\Textstem\Models\WranglerPage::class)
<a href="{{ route('wrangler-pages.create') }}">New page</a>
@endcan
// Programmatic check
if (Gate::allows('delete', $post)) {
// ...
}
Summary
| Scenario | Solution |
|---|---|
| Full admin access | Assign the super-admin role |
| Restrict to certain actions | Assign/remove Spatie permissions |
| Restrict to certain model instances | Create App\Domain\Admin\Auth\{Model} with allowed() |
| Filter the admin list | Add scopes() to the same class |
| Override a policy entirely | Extend the package policy and register with Gate::policy() |