Looking to hire Laravel developers? Try LaraJobs

laravel-gatetrail maintained by hangtuah

Description
Roles, permissions, and authorization for Laravel
Last update
2026/10/12 17:03 (dev-main)
License
Downloads
2

Comments
comments powered by Disqus

GateTrail

Roles, permissions, and authorization management for Laravel.

GateTrail records who holds which role, which permissions a role carries, which permissions a subject holds directly, and what a subject may do at a given moment. Every change goes through audited mutation services that require an actor and a reason, rather than through direct relationship writes.

Status

Public alpha, pre-1.0.0. The documented pre-v1 public API is frozen.

v0.1.0-alpha.3 is the first public alpha. It is intended for evaluation and controlled non-production use; it is not a stable release. Stable-release validation remains in progress.

The supported surface is defined by ADR 0014 and listed in docs/PUBLIC_API.md. The freeze prevents accidental drift; before 1.0.0, a deliberate breaking change to that surface still requires an ADR, a changelog entry and migration guidance. Track CHANGELOG.md when upgrading.

Requirements

Dependency Supported
PHP ^8.3
Laravel ^12.0 | ^13.0
MySQL 8.0.16 or later — verified against live 8.0.16 and 8.0.45
Oracle Database 12c Release 2 or later — verified against live Oracle Free 23.26.1
SQLite Used by the package's own test suite
PostgreSQL 14 or later — release 0001 DDL and read-only schema inspection verified on live 14 for both provisioning modes; application runtime remains unverified
SQL Server 2017 or later — query compilation and schema-adapter support, plus release 0001 DBA DDL smoke-tested on live SQL Server 2022; no schema inspector and no application runtime evidence

The behavioural suite runs on SQLite and live MySQL for both Laravel majors. Oracle runs a targeted live matrix for both generated-key strategies and both provisioning modes. PostgreSQL release 0001 install and rollback DDL and the read-only schema inspector have been exercised on a live local PostgreSQL 14 endpoint for both package-managed and DBA-managed provisioning, but no application behavioral suite has run there: no mutation, resolver, audit or concurrency path has executed against PostgreSQL. SQL Server release 0001 install and rollback DDL has been executed against a live disposable SQL Server 2022 database, which is schema-level artifact evidence only: SQL Server has no schema inspector yet, and no mutation, resolver, audit or concurrency path has run there either. PostgreSQL and SQL Server are not verified runtimes yet.

Installation

Require the exact public alpha so that a pre-release update is always an explicit decision:

composer require hangtuah/laravel-gatetrail:0.1.0-alpha.3

See docs/PRIVATE_COMPOSER_INSTALLATION.md only when installing from a private mirror. Applications that already have authorization tables or behavior should use the staged activation boundary described in that guide.

After Composer installation, the normal fresh-application setup is:

php artisan gatetrail:install

composer.json declares extra.laravel.providers, so Laravel package auto-discovery registers GateTrailServiceProvider automatically. No manual provider or alias registration is needed.

The install command publishes config/gatetrail.php if it does not already exist and, in the default package-managed mode, runs only GateTrail's seven migrations. It never overwrites an existing config file. Use --force to permit migrations in production, or --no-migrate to publish config without changing the database.

For DBA-managed provisioning, run gatetrail:install --no-migrate, set database.provisioning to dba, and have the database owner apply the matching versioned artifact. Running the command after dba is configured never runs package migrations.

When the configured schema, prefix, table names or Oracle generated-key objects differ from the committed canonical artifacts, generate an application-specific pair for DBA review without connecting to the database:

php artisan gatetrail:ddl \
    --driver=oracle \
    --output=database/gatetrail/oracle/0001/sequence-trigger

The command writes install.sql and rollback.sql, refuses to overwrite them unless --force is supplied, and uses the application's resolved GateTrail configuration.

Configuration

Publishing the configuration manually is optional; the package merges its own defaults, and gatetrail:install already publishes it safely. Without that command, publish it to change table names, models, guards, auditing or provisioning:

php artisan vendor:publish --tag=gatetrail-config

See the exhaustive docs/CONFIGURATION.md reference for every key, default, valid value, validation boundary and deployment consideration.

That writes config/gatetrail.php. gatetrail-config is the only publish tag GateTrail registers.

Database provisioning

gatetrail.database.provisioning selects one of two modes.

Package-managed ('package', the default) loads the seven GateTrail migrations from the package and creates the schema through php artisan migrate:

// config/gatetrail.php
return [
    'database' => [
        'provisioning' => 'package',
    ],
    // ...
];

DBA-managed ('dba') leaves DDL to a database owner outside the application deployment account. GateTrail's migrations do not execute, and the runtime account needs no create, alter or drop privilege. Versioned SQL artifacts are committed for the engines that have them:

database/ddl/mysql/0001/{install,rollback}.sql
database/ddl/oracle/0001/identity/{install,rollback}.sql
database/ddl/oracle/0001/sequence-trigger/{install,rollback}.sql
database/ddl/postgresql/0001/{install,rollback}.sql
database/ddl/sqlserver/0001/{install,rollback}.sql

Both modes produce the same schema from the same canonical manifest, and package behaviour does not depend on which one created it. See database/ddl/mysql/README.md, database/ddl/oracle/README.md, database/ddl/postgresql/README.md and database/ddl/sqlserver/README.md for installation and rollback procedures, and the consumer guide for the full comparison.

Diagnostics

Run the read-only consistency check after installation, an upgrade, or a known DBA intervention:

php artisan gatetrail:doctor
php artisan gatetrail:doctor --json

Doctor inspects the configured physical schema where an inspector exists and checks assignment periods, revocation tuples, definition references, guards and persistent coordination rows. It never migrates, repairs, revokes, deletes, backfills, locks or clears caches. An error finding or incomplete coverage returns a non-zero exit code. SQLite and SQL Server currently receive complete data checks but incomplete physical-schema coverage because no inspector exists for those drivers.

A clean result is only a best-effort consistency result. It cannot prove that no direct database write occurred or that no audit history is missing.

Compatibility matrix

Inspect the current PHP, Laravel and configured database-driver readiness alongside GateTrail's declared targets and recorded project evidence:

php artisan gatetrail:matrix
php artisan gatetrail:matrix --json

The command is read-only and makes no database connection. It distinguishes live behavioural evidence from test-only or schema-only evidence, so PostgreSQL and SQL Server are not presented as verified application runtimes. Its configured-database readiness check covers the Laravel driver and required local PHP extensions only; it cannot verify the database server version.

Minimal consumer application

examples/minimal-app is a retained Laravel 13 application that installs GateTrail through Composer package discovery and uses only the documented public API. Its automated SQLite vertical slice creates definitions, grants and assigns access, authorizes a real route, revokes the assignment, verifies denial, preserves history and inspects the resulting actor/reason audit records.

cd examples/minimal-app
composer install
composer test

This is executable consumer evidence and a learning aid, not a production starter kit or an expansion of the production database compatibility matrix.

Declaring a subject

A GateTrail subject is a persisted Eloquent model that declares a guard:

use HangTuah\GateTrail\Concerns\HasGateTrail;
use Illuminate\Foundation\Auth\User as Authenticatable;

final class User extends Authenticatable
{
    use HasGateTrail;

    protected string $gateTrailGuardName = 'web';
}

A subject that exposes a public gateTrailGuardName(): string method does not need the trait. Roles and permissions are namespaced by guard, so a subject can only be assigned definitions from its own guard.

A first mutation

Every mutation is performed through an injected contract, takes an actor and a reason, and returns a MutationResult. Names alone do not create definitions — create the role and the permission first, then connect them.

use HangTuah\GateTrail\Contracts\AssignmentManager;
use HangTuah\GateTrail\Contracts\PermissionManager;
use HangTuah\GateTrail\Contracts\RoleManager;
use HangTuah\GateTrail\SystemActor;

final readonly class GrantReviewerAccess
{
    public function __construct(
        private RoleManager $roles,
        private PermissionManager $permissions,
        private AssignmentManager $assignments,
    ) {}

    public function __invoke(User $user): void
    {
        $actor = SystemActor::named('access-bootstrap');

        $this->roles->create(
            name: 'content-reviewer',
            guard: 'web',
            actor: $actor,
            reason: 'Create the document review role',
        );

        $this->permissions->create(
            name: 'documents.approve',
            guard: 'web',
            actor: $actor,
            reason: 'Create the document approval permission',
        );

        $this->assignments->grantPermissionToRole(
            role: 'content-reviewer',
            permission: 'documents.approve',
            actor: $actor,
            reason: 'Reviewers approve submitted documents',
        );

        $this->assignments->assignRole(
            subject: $user,
            role: 'content-reviewer',
            actor: $actor,
            reason: 'Joined the document review rota',
        );
    }
}

A first permission check

PermissionResolver is the only supported way to ask what a subject may do. It combines direct grants and role-derived grants, applies assignment lifecycle, and denies by default.

use HangTuah\GateTrail\Contracts\PermissionResolver;

final readonly class DocumentApprovalCheck
{
    public function __construct(
        private PermissionResolver $permissions,
    ) {}

    public function allowed(User $user): bool
    {
        return $this->permissions->allows(
            subject: $user,
            permission: 'documents.approve',
        );
    }
}

RoleResolver answers the equivalent question for roles.

Laravel's normal authorization API uses the same resolver automatically for argument-free permission abilities:

$user->can('documents.approve');
Gate::forUser($user)->allows('documents.approve');
Gate::authorize('documents.approve');

Native Blade @can / @cannot directives use that same decision. Checks with resource arguments, such as $user->can('update', $document), continue to the application's normal gates and policies.

Routes can enforce the same permission or role set through the six automatic aliases:

Route::post('/documents/{document}/approve', DocumentApprovalController::class)
    ->middleware([
        'auth',
        'gatetrail.permission:documents.approve',
    ]);

Route::get('/review', ReviewQueueController::class)
    ->middleware('gatetrail.role.any:reviewer,publisher');

Use the .any alias when one listed name is sufficient and .all when every listed name is required. The same forms exist for gatetrail.permission and gatetrail.role.

Models using HasGateTrail also receive concise helpers backed by the same resolvers:

$user->hasRole('reviewer');
$user->hasAnyRole(['reviewer', 'publisher']);
$user->hasPermissionTo('documents.approve');
$user->effectivePermissions();
$user->roles();

Public versus internal API

Depend on the contracts, value types, events and exceptions listed in docs/PUBLIC_API.md, and nothing else.

A class marked @internal is not consumer API. PSR-4 makes all 252 package classes loadable and the container can build most of them, but 184 of them carry a class-level @internal annotation and may be renamed, recomposed or deleted in any release, including a patch. That includes the Default* manager and resolver implementations behind the contracts — inject AssignmentManager, never DefaultAssignmentManager.

Not yet implemented

These are designed or planned, and are not available today. Do not build against them:

  • GateTrail-supplied resource policies;
  • custom GateTrail Blade directives—use Laravel's native @can / @cannot;
  • other planned GateTrail console commands, including gatetrail:cache-clear;
  • the UnauthorizedMutation exception type;
  • cross-request cache and cross-request invalidation APIs;
  • an administration UI;
  • wildcard permissions, role hierarchy, and tenant- or team-scoped roles.

Security

GateTrail records the supplied mutation actor for attribution; it does not authorize that actor. Consumer controllers, jobs and services must authorize the operation with Laravel policies or Gates before calling a GateTrail manager.

The security model and its tested boundaries are documented in docs/security/threat-model.md. Report a suspected vulnerability privately by following SECURITY.md, not through a public issue.

Documentation

  • docs/CONSUMER_GUIDE.md — installation, provisioning, mutations, checks, auditing, events and error handling.
  • docs/CONFIGURATION.md — every supported key, default, validation rule and deployment consideration.
  • docs/UPGRADING.md — safe package-managed and DBA-managed upgrades, configuration reconciliation and rollback planning.
  • docs/PUBLIC_API.md — the authoritative supported symbol manifest and exact signatures.
  • PACKAGE_SPEC.md — the full package specification.
  • ROADMAP.md — phase progress and verification evidence.
  • CHANGELOG.md — released and unreleased changes.

License

GateTrail is released under the MIT License. See LICENSE.