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:

  1. Super-admin bypass -- users with the super-admin role pass all gates unconditionally.
  2. Role-based permissions -- standard Spatie permissions checked in each policy method.
  3. 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()
esc