akindutire / authorization-pkg
A modern, attribute-based authorization package for Laravel 9+ that works with any Eloquent model using PHP 8 attributes
Requires
- php: ^8.1|^8.2|^8.3
- illuminate/database: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^9.0|^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^9.0|^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- mockery/mockery: ^1.5
- orchestra/testbench: ^7.0|^8.0|^9.0|^10.0|^11.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^9.5|^10.0|^11.0
This package is auto-updated.
Last update: 2026-07-06 11:26:07 UTC
README
A modern, attribute-based authorization package for Laravel 9+ that works with any Eloquent model using PHP 8 attributes. Unlike traditional user-centric permission systems, this package lets you attach abilities directly to any model - Articles, Organizations, TeamMembers, or any entity in your application.
Built for scale - optimized for applications with millions of entities.
Why Model-Agnostic?
Traditional systems: User → has Permissions → to perform Actions → on Resources
This package: Subject (any model) → has Abilities → validated via Attributes
// ❌ Traditional: User-centric - doesn't model entity-specific abilities $user->hasPermission('edit-article'); // ✅ This package: Model-agnostic - abilities belong to entities #[HasAny(['can_edit'], Article::class, 'id')] public function update(int $articleId) { // The ARTICLE itself has 'can_edit' ability // The entity being modified controls who can modify it }
Key Concepts
- Subject: Any Eloquent model with the
HasPermissionstrait (User, Article, TeamMember, Organization, etc.) - Abilities: Actions a subject can perform (
can_edit,can_publish,can_invite) - Attributes: PHP 8 attributes declaring ability requirements (
#[HasAny],#[HasAll]) - Validation: Automatic checking via
ValidateSubjectActionmiddleware
How It Works: Architecture Flow
Controller Method
↓
[Attribute Declaration]
#[HasAny(['can_edit'], Article::class, 'id')]
↓
[Middleware: ValidateSubjectAction]
↓
[Subject Resolution]
- Lookup model by property: Article::where('id', $value)
- Extract abilities from database columns
↓
[Permission Service]
- Check if subject has required abilities
- Respect revoked abilities
↓
[Cache Result]
- Multi-layer caching for performance
↓
[Allow/Deny Request]
Terminology: Why "Abilities"?
In code: The package uses permissions (columns/methods) for backward compatibility with Laravel conventions.
In concept: We call these abilities because:
- Model-agnostic: "Article has ability to be edited" (not "Article has permission")
- Subject-focused: Abilities belong to the entity being authorized
- Industry standard: CanCanCan (Rails) and CASL (JavaScript) use "abilities"
- Clear semantics: Describes what subjects can do
| Alternative | Why Not? |
|---|---|
| Capabilities | Conflicts with PHP extensions |
| Grants | Sounds temporary |
| Actions | Too generic, conflicts with controllers |
| Permissions | Implies user-centric (kept in code for compatibility) |
Features
- 🎯 Model-agnostic - Works with any Eloquent model, not just Users
- 🏷️ Attribute-based - Declarative authorization using PHP 8 attributes
- ⚡ Auto-invalidating cache - Detects attribute changes without manual clearing
- 🔍 Flexible resolution - Lookup subjects by any property (id, uuid, email, slug)
- 🎭 Revocation support - Explicitly deny abilities that override grants
- 🎨 Facade included - Easy-to-use facade for programmatic checks
- 🔒 Middleware validation - Automatic checks using reflection
- 🚀 Production-ready - Multi-layer caching, optimized for 500M+ entities
- ⚛️ Race condition safe - Atomic updates using database operations
Performance Highlights
- 8-25ms permission check latency (p50) at scale
- 98% reduction in database queries via intelligent caching
- 99%+ cache hit rate in production environments
- Zero runtime reflection overhead via metadata caching
- Horizontal scalability - tested with 500M entities
📊 See SCALABILITY.md for detailed benchmarks.
Quick Start
1. Install
composer require akindutire/authorization-pkg
2. Add Abilities to Any Model
use Akindutire\Authorization\Traits\HasPermissions; // Works with ANY Eloquent model class Article extends Model { use HasPermissions; protected $casts = [ 'allowed_permissions' => 'array', // What this article can do 'revoked_permissions' => 'array', // Explicitly denied ]; } class TeamMember extends Model { use HasPermissions; protected $casts = [ 'allowed_permissions' => 'array', // What this member can do 'revoked_permissions' => 'array', ]; } class Organization extends Model { use HasPermissions; protected $casts = [ 'allowed_permissions' => 'array', // What this org can do 'revoked_permissions' => 'array', ]; }
3. Grant Abilities
// Articles have publication abilities $article = Article::find(1); $article->grantPermission(['can_be_edited', 'can_be_published']); // Team members have role-based abilities $member = TeamMember::find(1); $member->grantPermission(['can_invite', 'can_manage_billing']); // Organizations have feature abilities $org = Organization::find(1); $org->grantPermission(['can_use_api', 'can_white_label']);
4. Protect Controller Methods with Attributes
use Akindutire\Authorization\Attributes\{HasAny, HasAll}; use Akindutire\Authorization\Attributes\SubjectValue; class ArticleController { // Check if Article has 'can_be_edited' ability #[HasAny(['can_be_edited'], Article::class, 'id')] public function update( #[SubjectValue] int $id, Request $request ) { // Only executes if the Article can be edited $article = Article::find($id); $article->update($request->all()); } // Article must have BOTH abilities #[HasAll(['can_be_published', 'can_be_featured'], Article::class, 'id')] public function publish(#[SubjectValue] int $id) { Article::find($id)->update(['published_at' => now()]); } // Works with any property, not just 'id' #[HasAny(['can_be_viewed'], Article::class, 'slug')] public function show(#[SubjectValue] string $slug) { return Article::where('slug', $slug)->firstOrFail(); } } class TeamController { // Check TeamMember abilities #[HasAny(['can_invite'], TeamMember::class, 'member_id')] public function invite( #[SubjectValue('member_id')] Request $request ) { // TeamMember must have 'can_invite' ability } }
5. Register Middleware
// app/Http/Kernel.php protected $middlewareAliases = [ 'validate.subject.action' => \Akindutire\Authorization\Middleware\ValidateSubjectAction::class, ]; // routes/web.php Route::middleware(['validate.subject.action'])->group(function () { Route::put('/articles/{id}', [ArticleController::class, 'update']); Route::post('/articles/{id}/publish', [ArticleController::class, 'publish']); });
Use Cases
1. Content Management Systems
// Articles control their own editability $article->grantPermission(['can_be_edited', 'can_be_deleted']); $article->revokePermission(['can_be_deleted']); // Make read-only #[HasAny(['can_be_edited'], Article::class, 'id')] public function update(int $id) { }
2. Multi-Tenant SaaS
// Organizations have feature abilities $org->grantPermission(['can_use_api', 'can_export_data', 'can_white_label']); #[HasAll(['can_use_api'], Organization::class, 'org_id')] public function apiAccess(#[SubjectValue] int $org_id) { }
3. Team Collaboration
// Team members have role-based abilities $member->grantPermission(['can_invite', 'can_view_analytics', 'can_manage_billing']); #[HasAny(['can_invite', 'can_manage_team'], TeamMember::class, 'id')] public function addMember(#[SubjectValue] int $id) { }
4. Resource Sharing
// Documents have sharing abilities $document->grantPermission(['can_be_shared', 'can_be_commented']); #[HasAny(['can_be_shared'], Document::class, 'uuid')] public function share(#[SubjectValue] string $uuid) { }
Core API
Granting and Revoking Abilities
// Grant abilities (single or multiple) $subject->grantPermission('can_edit'); $subject->grantPermission(['can_edit', 'can_delete']); // Revoke abilities (explicitly deny) $subject->revokePermission('can_delete'); $subject->revokePermission(['can_delete', 'can_admin']); // Check abilities $subject->hasPermission('can_edit'); // Check single $subject->hasAnyPermission(['can_edit', 'can_view']); // Has any $subject->hasAllPermissions(['can_edit', 'can_publish']); // Has all // Get abilities $allowed = $subject->getAllowedPermissions(); // ['can_edit', 'can_view'] $revoked = $subject->getRevokedPermissions(); // ['can_delete'] $effective = $subject->getEffectivePermissions(); // Allowed minus revoked
Facade Usage
use Akindutire\Authorization\Facades\EntityPermission; // Check if subject has abilities $canEdit = EntityPermission::subject($article)->hasAny(['can_be_edited']); $canPublish = EntityPermission::subject($article)->hasAll(['can_be_published', 'can_be_featured']); // Get role-based ability templates from config $ownerAbilities = EntityPermission::getAbilities('owner');
Attribute Options
// Basic usage #[HasAny(['can_edit'], Article::class, 'id')] // Custom property resolution #[HasAny(['can_edit'], Article::class, 'uuid')] #[HasAny(['can_edit'], Article::class, 'slug')] // Extract subject value from request #[HasAny(['can_edit'], Article::class, 'id')] public function update( #[SubjectValue('article_id')] Request $request ) { } // Require all abilities #[HasAll(['can_publish', 'can_feature'], Article::class, 'id')]
Configuration
Ability Templates (Optional)
Define reusable ability templates in config:
// config/akindutire-authorization.php 'abilities' => [ 'article_editor' => ['can_be_edited', 'can_be_deleted'], 'article_publisher' => ['can_be_edited', 'can_be_published', 'can_be_featured'], 'team_admin' => ['can_invite', 'can_remove', 'can_manage_billing'], 'team_member' => ['can_invite', 'can_view_analytics'], ],
Then use them:
$article->grantPermission(EntityPermission::getAbilities('article_publisher')); $member->grantPermission(EntityPermission::getAbilities('team_admin'));
Custom Column Names
'column_names' => [ 'allowed_permissions' => 'abilities', // Rename columns 'revoked_permissions' => 'denied_abilities', ],
Cache Configuration
'entity_cache_ttl' => 300, // 5 minutes 'reflection_cache_enabled' => true, 'auto_invalidate_reflection_cache' => true,
Database Setup
Generate Migrations
php artisan make:permission-migration articles php artisan make:permission-migration team_members php artisan make:permission-migration organizations
This creates:
Schema::table('articles', function (Blueprint $table) { $table->json('allowed_permissions')->nullable(); $table->json('revoked_permissions')->nullable(); // Database-specific indexes for optimal performance });
Run Migrations
php artisan migrate
Advanced Examples
Complex Controller
class ArticleController { #[HasAny(['can_be_edited'], Article::class, 'id')] public function update(#[SubjectValue] int $id, Request $request) { $article = Article::find($id); $article->update($request->validated()); return response()->json($article); } #[HasAll(['can_be_published', 'can_be_featured'], Article::class, 'id')] public function publishAndFeature(#[SubjectValue] int $id) { $article = Article::find($id); $article->update([ 'published_at' => now(), 'featured' => true, ]); return response()->json($article); } #[HasAny(['can_be_viewed'], Article::class, 'slug')] public function showBySlug(#[SubjectValue] string $slug) { return Article::where('slug', $slug)->firstOrFail(); } }
Dynamic Ability Management
// Grant abilities based on business logic if ($user->isAdmin()) { $article->grantPermission(['can_be_edited', 'can_be_deleted', 'can_be_published']); } elseif ($user->isEditor()) { $article->grantPermission(['can_be_edited']); } // Temporarily revoke abilities $article->revokePermission(['can_be_deleted']); // Lock article // Later restore $article->grantPermission(['can_be_deleted']); // Removes from revoked
Multi-Property Resolution
// By ID #[HasAny(['can_edit'], User::class, 'id')] public function updateById(#[SubjectValue] int $id) { } // By UUID #[HasAny(['can_edit'], User::class, 'uuid')] public function updateByUuid(#[SubjectValue] string $uuid) { } // By email #[HasAny(['can_login'], User::class, 'email')] public function authenticate(#[SubjectValue] string $email) { }
Requirements
- PHP 8.1 or higher
- Laravel 9.0 or higher
- Redis or Memcached (recommended for production)
Documentation
Why This Package?
| Traditional Systems | This Package |
|---|---|
| User-centric | Model-agnostic |
| Permissions on users | Abilities on any model |
$user->can('edit-post') |
#[HasAny(['can_edit'], Article::class)] |
| Doesn't scale to entity-specific rules | Entity owns its abilities |
| Complex policy classes | Declarative attributes |
Comparison
| Feature | Spatie Permission | Laravel Gates | This Package |
|---|---|---|---|
| Model-agnostic | ❌ User-only | ❌ User-only | ✅ Any model |
| Attribute-based | ❌ | ❌ | ✅ |
| Auto-caching | ⚠️ Manual | ❌ | ✅ Auto-invalidating |
| Flexible resolution | ❌ ID only | ❌ | ✅ Any property |
| Revocation support | ❌ | ⚠️ Limited | ✅ Native |
| Scale (entities) | < 1M | N/A | 500M+ tested |
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
Security
If you discover any security issues, please email security@example.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.