Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
36f2b25
chore: add .worktrees/ to .gitignore
christopherarter Apr 9, 2026
f8d9a3f
feat: add Effect and Decision enums for v2 evaluation
christopherarter Apr 9, 2026
584a6a8
feat: add v2 core DTOs (Principal, Resource, Context, PolicyStatement…
christopherarter Apr 9, 2026
ea2a806
feat: add EvaluationRequest, rewrite EvaluationResult as DTO, remove …
christopherarter Apr 9, 2026
17caae8
feat: add PolicyResolver contract and IdentityPolicyResolver
christopherarter Apr 9, 2026
3b4061e
feat: rewrite Evaluator contract and DefaultEvaluator with resolver c…
christopherarter Apr 9, 2026
b43f178
feat: rewrite CachedEvaluator for EvaluationRequest-based cache keys
christopherarter Apr 9, 2026
516dc41
feat: rewrite HasPermissions trait with v2 evaluation pipeline
christopherarter Apr 9, 2026
c5b3f16
feat: rewrite ServiceProvider with resolver chain wiring and v2 Gate …
christopherarter Apr 9, 2026
32ec5b8
fix: update all tests for v2 evaluation types and contracts
christopherarter Apr 9, 2026
35849a9
feat: add BoundaryPolicyResolver to extract boundary logic into resol…
christopherarter Apr 9, 2026
abb11ff
feat: add condition system with registry, 5 built-in evaluators, and …
christopherarter Apr 9, 2026
9e71797
feat: add resource policies — migration, model, store, resolver, and …
christopherarter Apr 9, 2026
e99fe97
feat: add SanctumPolicyResolver to extract token scoping into resolver
christopherarter Apr 9, 2026
ec5fb05
feat: update document system for v2 format with conditions and resour…
christopherarter Apr 9, 2026
68906e3
chore: final v2 cleanup — remove dead references, fix imports
christopherarter Apr 9, 2026
6732a99
docs: update policy document format documentation for v2
christopherarter Apr 9, 2026
d0f26bc
docs: update existing documentation for v2 API changes
christopherarter Apr 9, 2026
ec65cd5
docs: add documentation for conditions, resource policies, and custom…
christopherarter Apr 9, 2026
dedc5b8
fix: use order-independent assertion for MySQL JSON key ordering
christopherarter Apr 9, 2026
4cdb5b6
fix: resolve all PHPStan level 9 errors across v2 condition and resol…
christopherarter Apr 9, 2026
f7066b4
fix: resolve remaining PHPStan errors for PHP 8.4 compatibility
christopherarter Apr 9, 2026
87afb85
fix: resolve PHPStan errors for PHP 8.4 and remove version labels fro…
christopherarter Apr 9, 2026
9197603
chore: upgrade Pest to v4 for Laravel 13 compatibility
christopherarter Apr 9, 2026
a2c83ca
fix: map pgsql database matrix value to postgres compose service name
christopherarter Apr 9, 2026
9fba1a4
ci: add Composer vendor caching to all CI jobs
christopherarter Apr 9, 2026
b0e2201
fix: exclude benchmark tests from CI to prevent MySQL OOM
christopherarter Apr 9, 2026
ab273a2
fix: tune MySQL container for CI memory constraints
christopherarter Apr 9, 2026
a87a902
ci: skip workflow when only docs or markdown files change
christopherarter Apr 9, 2026
0c666b6
fix: replace MySQL 8.4 with MariaDB 11 for CI stability
christopherarter Apr 9, 2026
434c029
fix: revert to MySQL 8.4 with memory tuning instead of MariaDB
christopherarter Apr 9, 2026
c4b21ee
chore: consolidate docker-compose files and revert to tuned MySQL 8.4
christopherarter Apr 9, 2026
3aadb62
ci: drop MySQL from CI matrix and bump actions to Node.js 24
christopherarter Apr 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 45 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,33 @@ name: CI

on:
pull_request:
paths-ignore:
- '**/*.md'
- 'docs/**'
- 'LICENSE'

env:
COMPOSER_PROCESS_TIMEOUT: 0
COMPOSER_NO_INTERACTION: 1
COMPOSER_NO_AUDIT: 1

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'

- uses: actions/cache@v5
with:
path: vendor
key: composer-lint-${{ hashFiles('composer.json') }}
restore-keys: composer-lint-

- run: composer install --no-interaction

- run: vendor/bin/pint --test
Expand All @@ -23,12 +38,18 @@ jobs:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'

- uses: actions/cache@v5
with:
path: vendor
key: composer-static-${{ hashFiles('composer.json') }}
restore-keys: composer-static-

- run: composer install --no-interaction

- run: vendor/bin/phpstan analyse
Expand All @@ -43,22 +64,28 @@ jobs:
php: ['8.4', '8.5']
laravel: ['12', '13']
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: pcov
extensions: redis

- uses: actions/cache@v5
with:
path: vendor
key: composer-test-${{ matrix.php }}-${{ matrix.laravel }}-${{ hashFiles('composer.json') }}
restore-keys: composer-test-${{ matrix.php }}-${{ matrix.laravel }}-

- run: composer require illuminate/support:^${{ matrix.laravel }}.0 --no-update
- run: composer update --no-interaction

- run: docker compose -f docker-compose.test.yml up -d --wait valkey
- run: docker compose up -d --wait valkey

- run: vendor/bin/pest --coverage --min=90
- run: vendor/bin/pest --exclude-group=benchmark --coverage --min=90

- run: docker compose -f docker-compose.test.yml down -v
- run: docker compose down -v

test-database:
name: Tests (${{ matrix.database }} / PHP ${{ matrix.php }} / Laravel ${{ matrix.laravel }})
Expand All @@ -69,20 +96,26 @@ jobs:
matrix:
php: ['8.4', '8.5']
laravel: ['12', '13']
database: [pgsql, mysql]
database: [pgsql]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: redis, pdo_pgsql, pdo_mysql
extensions: redis, pdo_pgsql

- uses: actions/cache@v5
with:
path: vendor
key: composer-db-${{ matrix.php }}-${{ matrix.laravel }}-${{ hashFiles('composer.json') }}
restore-keys: composer-db-${{ matrix.php }}-${{ matrix.laravel }}-

- run: composer require illuminate/support:^${{ matrix.laravel }}.0 --no-update
- run: composer update --no-interaction

- run: docker compose -f docker-compose.test.yml up -d --wait ${{ matrix.database }} valkey
- run: docker compose up -d --wait postgres valkey

- run: DB_CONNECTION=${{ matrix.database }} vendor/bin/pest
- run: DB_CONNECTION=${{ matrix.database }} vendor/bin/pest --exclude-group=benchmark

- run: docker compose -f docker-compose.test.yml down -v
- run: docker compose down -v
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ composer.lock
.claude/
CLAUDE.md
/backlog
/docs/superpowers
/docs/superpowers
.worktrees/
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Policy Engine for Laravel

[![Pint](https://img.shields.io/github/actions/workflow/status/dynamik-dev/laravel-policy-engine/ci.yml?branch=main&label=Pint&logo=laravel)](https://github.com/dynamik-dev/laravel-policy-engine/actions/workflows/ci.yml)
[![Larastan](https://img.shields.io/github/actions/workflow/status/dynamik-dev/laravel-policy-engine/ci.yml?branch=main&label=Larastan&logo=php)](https://github.com/dynamik-dev/laravel-policy-engine/actions/workflows/ci.yml)
[![Pest](https://img.shields.io/github/actions/workflow/status/dynamik-dev/laravel-policy-engine/ci.yml?branch=main&label=Pest&logo=php)](https://github.com/dynamik-dev/laravel-policy-engine/actions/workflows/ci.yml)

An IAM-style policy engine for Laravel. Define your authorization as declarative JSON documents and import them the way you'd manage AWS IAM policies.

```json
Expand Down Expand Up @@ -106,16 +110,15 @@ Every component is coded to a PHP interface. Swap any implementation via the ser

## Requirements

| Dependency | Supported Versions |
|---|---|
| PHP | 8.4, 8.5 |
| Laravel | 12, 13 |
| PostgreSQL | 17+ |
| MySQL | 8.4+ |
| SQLite | 3.35+ |
| Valkey / Redis | 8+ |
| Dependency | Supported Versions |
| -------------- | ------------------ |
| PHP | 8.4, 8.5 |
| Laravel | 12, 13 |
| PostgreSQL | 17+ |
| SQLite | 3.35+ |
| Valkey / Redis | 8+ |

SQLite works out of the box for development. PostgreSQL, MySQL, and Valkey are optional — the package tests against all of them in CI.
SQLite works out of the box for development. PostgreSQL and Valkey are optional — the package tests against both in CI. MySQL is not officially supported but should work fine since Laravel's query builder abstracts the differences.

## Documentation

Expand Down
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
"laravel/pint": "^1.27",
"laravel/sanctum": "^4.3",
"orchestra/testbench": "^10.0|^11.0",
"pestphp/pest": "^3.0",
"pestphp/pest-plugin-laravel": "^3.0",
"pestphp/pest-plugin-mutate": "^3.0"
"pestphp/pest": "^4.0",
"pestphp/pest-plugin-laravel": "^4.0",
"pestphp/pest-plugin-mutate": "^4.0"
},
"autoload": {
"psr-4": {
Expand Down
38 changes: 13 additions & 25 deletions config/policy-engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,32 @@

declare(strict_types=1);

use DynamikDev\PolicyEngine\Resolvers\BoundaryPolicyResolver;
use DynamikDev\PolicyEngine\Resolvers\IdentityPolicyResolver;
use DynamikDev\PolicyEngine\Resolvers\ResourcePolicyResolver;
use DynamikDev\PolicyEngine\Resolvers\SanctumPolicyResolver;

return [
'cache' => [
'enabled' => true,

// WARNING: If your cache driver does not support tags (file, database),
// policy-engine uses generation counters for invalidation. Entries expire
// naturally via TTL rather than being deleted immediately. For instant
// invalidation, use a tagged driver (Redis, Memcached).
'store' => env('POLICY_ENGINE_CACHE_STORE', 'default'),

// TTL in seconds.
// Lower values reduce the window for stale cached permissions after revocation.
'ttl' => 60 * 5,
],
'protect_system_roles' => true,
'log_denials' => true,
'explain' => env('POLICY_ENGINE_EXPLAIN', false),
'trace' => env('POLICY_ENGINE_TRACE', false),
'deny_unbounded_scopes' => false,

// When true, boundary checks are applied even for unscoped (global)
// evaluations. By default, global assignments are inherently unbounded —
// boundaries only restrict scoped checks. Enable this if you need to
// enforce boundary ceilings on global permission checks. Be aware that
// this requires boundaries to exist for every scope the subject might
// access, which may cause unexpected denials.
'enforce_boundaries_on_global' => false,

// Prefix prepended to all policy-engine table names. Useful when another
// package (e.g. Spatie Permission, Bouncer) already uses generic names
// like "permissions" or "roles". Set to 'pe_' for new installs alongside
// other permission packages. Empty string preserves backwards compatibility.
'table_prefix' => '',

// The seeder class used by the policy-engine:sync command.
// Must be resolvable by db:seed --class (typically in Database\Seeders namespace).
'seeder_class' => 'PermissionSeeder',

'document_path' => null,
'gate_passthrough' => [],
'import_subject_types' => [],

'resolvers' => [
IdentityPolicyResolver::class,
BoundaryPolicyResolver::class,
ResourcePolicyResolver::class,
SanctumPolicyResolver::class,
],
];
33 changes: 33 additions & 0 deletions database/migrations/0009_create_resource_policies_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
$prefix = config('policy-engine.table_prefix', '');

Schema::create($prefix.'resource_policies', function (Blueprint $table): void {
$table->ulid('id')->primary();
$table->string('resource_type');
$table->string('resource_id')->nullable();
$table->string('effect');
$table->string('action');
$table->string('principal_pattern')->nullable();
$table->json('conditions')->nullable();
$table->timestamps();

$table->index(['resource_type', 'resource_id']);
});
}

public function down(): void
{
Schema::dropIfExists(config('policy-engine.table_prefix', '').'resource_policies');
}
};
39 changes: 0 additions & 39 deletions docker-compose.test.yml

This file was deleted.

28 changes: 23 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
services:
test:
build:
context: .
dockerfile: docker/Dockerfile
command: vendor/bin/pest --coverage
valkey:
image: valkey/valkey:8-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 2s
timeout: 3s
retries: 5

postgres:
image: postgres:17-alpine
ports:
- "5432:5432"
environment:
POSTGRES_DB: policy_engine_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test"]
interval: 2s
timeout: 3s
retries: 5
15 changes: 0 additions & 15 deletions docker/Dockerfile

This file was deleted.

Loading