Skip to main content
Glama

OpenFGA MCP

LARAVEL_SDK.md246 kB
# LARAVEL_SDK Documentation > Compiled from: https://github.com/evansims/openfga-laravel > Generated: 2025-08-03 22:14:06 UTC --- <!-- Source: evansims/openfga-laravel/README.md --> <div align="center"> <p><a href="https://openfga.dev"><img src=".github/openfga.png" width="100" /></a></p> <h1>OpenFGA Laravel SDK</h1> <p>Stop writing authorization logic. Start asking questions.</p> </div> <p><br /></p> **Every app needs permissions.** Most developers end up with authorization logic scattered across controllers, middleware, and business logic. Changes break things. New features require touching dozens of files. **[OpenFGA](https://openfga.dev/) solves this.** Define your authorization rules once, query them anywhere. This package provides complete integration of [OpenFGA](https://openfga.dev/) and [Auth0 FGA](https://auth0.com/fine-grained-authorization) for Laravel applications. - **Eloquent Integration** - Authorization methods on your models - **Middleware Protection** - Secure routes with permission checks - **Blade Directives** - Show/hide UI based on permissions - **Testing Utilities** - Fake permissions in your tests - **Performance Optimized** - Built-in caching and batch operations - **Queue Support** - Async permission operations - **Multi-tenancy Ready** - Multiple stores and connections - **Type Safe** - PHP 8.3+ with strict typing and comprehensive generics - **Developer Friendly** - Enhanced IDE support with detailed PHPDoc annotations <p><br /></p> ### Installation ```bash composer require evansims/openfga-laravel ``` Publish the configuration: ```bash php artisan vendor:publish --tag="openfga-config" ``` Set your environment variables: ```env OPENFGA_URL=http://localhost:8080 OPENFGA_STORE_ID=your-store-id ``` <p><br /></p> ### Usage Patterns ```php // Controllers - Type-safe permission checks if (cannot('edit', $document)) { abort(403); } // Middleware - Strict parameter validation Route::put('/documents/{document}', [DocumentController::class, 'update']) ->middleware('openfga:editor,document:{document}'); // Blade Views - Enhanced type safety @can('edit', 'document:' . $document->id) <button>Edit</button> @endcan // Eloquent Models - Comprehensive type annotations $document->grant($user, 'editor'); // Grant permission $document->check($user, 'editor'); // Check permission $document->revoke($user, 'editor'); // Revoke permission // Query by permissions - Generic return types $myDocuments = Document::whereUserCan($user, 'edit')->get(); ``` <p><br /></p> ### Quickstart Let's implement a simple document sharing system with enhanced type safety. ```php <?php declare(strict_types=1); use App\Models\Document; class DocumentController extends Controller { /** * Share a document with another user. */ public function share(Request $request, Document $document): RedirectResponse { // Ensure user can share (only owners can share) $this->authorize('owner', $document); // Grant permission to new user $document->grant($request->user_email, $request->permission); return back()->with('success', 'Document shared successfully!'); } /** * List documents the user can view. */ public function index(): View { $documents = Document::whereUserCan(auth()->user(), 'viewer') ->latest() ->paginate(); return view('documents.index', compact('documents')); } } ``` <p><br /></p> ### Documentation - [Installation](https://github.com/evansims/openfga-laravel/blob/main/docs/installation.md) - [Quickstart](https://github.com/evansims/openfga-laravel/blob/main/docs/quickstart.md) - [Configuration](https://github.com/evansims/openfga-laravel/blob/main/docs/configuration.md) - [Eloquent Integration](https://github.com/evansims/openfga-laravel/blob/main/docs/eloquent.md) - [Middleware](https://github.com/evansims/openfga-laravel/blob/main/docs/middleware.md) - [Performance & Optimization](https://github.com/evansims/openfga-laravel/blob/main/docs/performance.md) - [Testing](https://github.com/evansims/openfga-laravel/blob/main/docs/testing.md) - [API Reference](https://github.com/evansims/openfga-laravel/blob/main/docs/api-reference.md) - **[Cookbook & Recipes](https://github.com/evansims/openfga-laravel/blob/main/docs/cookbook/README.md)** - Real-world implementation patterns <p><br /></p> ### Related - [OpenFGA PHP SDK](https://github.com/evansims/openfga-php) <p><br /></p> ### Contributing Contributions are welcome—have a look at our [contributing guidelines](https://github.com/evansims/openfga-laravel/blob/main/.github/CONTRIBUTING.md). <!-- End of evansims/openfga-laravel/README.md --> <!-- Source: evansims/openfga-laravel/docs/cache/read-through.md --> ### Read-Through Cache The OpenFGA Laravel package includes a powerful read-through cache implementation that automatically fetches and caches permissions when they're not in cache, providing transparent caching behavior. #### Overview Read-through caching is a caching pattern where: - Cache hits return data immediately from cache - Cache misses automatically fetch from the source and populate the cache - The application doesn't need to manage cache population explicitly #### Features - **Automatic cache population** - No manual cache warming needed - **Negative result caching** - Cache "false" results with shorter TTL - **Error caching** - Prevent hammering the API when errors occur - **Contextual bypass** - Skip caching for requests with context - **Cache metrics** - Track hit rates and performance - **Tagged cache support** - Granular cache invalidation #### Configuration ```php // config/openfga.php 'cache' => [ 'enabled' => true, 'store' => null, // Use default cache store 'ttl' => 300, // 5 minutes 'prefix' => 'openfga', // Read-through cache settings 'read_through' => true, 'negative_ttl' => 60, // Cache negative results for 1 minute 'error_ttl' => 10, // Cache errors for 10 seconds 'log_misses' => false, // Log cache misses for debugging // Cache metrics 'metrics' => [ 'enabled' => true, ], ], ``` #### Usage The read-through cache is automatically used by the OpenFgaManager when enabled: ```php use OpenFGA\Laravel\OpenFgaManager; $manager = app(OpenFgaManager::class); // First call - cache miss, fetches from API $allowed = $manager->check('user:123', 'viewer', 'document:456'); // Second call - cache hit, returns from cache $allowed = $manager->check('user:123', 'viewer', 'document:456'); ``` ##### Direct Access You can also access the read-through cache directly: ```php $readThroughCache = $manager->getReadThroughCache(); // Check permission with caching $allowed = $readThroughCache->check( 'user:123', 'viewer', 'document:456' ); // List objects with caching $objects = $readThroughCache->listObjects( 'user:123', 'viewer', 'document' ); ``` #### Cache Behavior ##### Positive Results When a permission check returns `true`, it's cached for the configured TTL: ```php // Cached for 5 minutes (default TTL) $allowed = $manager->check('user:123', 'editor', 'document:456'); // true ``` ##### Negative Results When a permission check returns `false`, it's cached for a shorter duration: ```php // Cached for 1 minute (negative_ttl) $allowed = $manager->check('user:123', 'admin', 'document:456'); // false ``` ##### Error Handling Errors are cached briefly to prevent API hammering: ```php try { // If API is down, error is cached for 10 seconds $allowed = $manager->check('user:123', 'viewer', 'document:456'); } catch (\Exception $e) { // Subsequent calls within 10 seconds won't hit the API } ``` ##### Contextual Requests Requests with contextual tuples or context are never cached: ```php // Not cached - has contextual tuples $allowed = $manager->check( 'user:123', 'viewer', 'document:456', $contextualTuples ); // Not cached - has context $allowed = $manager->check( 'user:123', 'viewer', 'document:456', [], ['ip' => '192.168.1.1'] ); ``` #### Cache Invalidation The read-through cache integrates with the tagged cache for intelligent invalidation: ```php $cache = $manager->getReadThroughCache(); // Invalidate all cache entries for a user $cache->invalidate('user:123'); // Invalidate all cache entries for an object $cache->invalidate(null, null, 'document:456'); // Invalidate all cache entries for a relation $cache->invalidate(null, 'viewer'); // Invalidate a specific permission $cache->invalidate('user:123', 'viewer', 'document:456'); ``` #### Cache Metrics Track cache performance with built-in metrics: ```php $cache = $manager->getReadThroughCache(); // Get cache statistics $stats = $cache->getStats(); // [ // 'hits' => 150, // 'misses' => 50, // 'hit_rate' => 75.0 // ] // Reset statistics $cache->resetStats(); ``` ##### Using the CLI ```bash # View cache statistics php artisan openfga:cache:stats # Output as JSON php artisan openfga:cache:stats --json # Reset statistics php artisan openfga:cache:stats --reset ``` #### Debugging Enable cache miss logging for debugging: ```php // .env OPENFGA_CACHE_LOG_MISSES=true ``` This will log all cache misses: ``` [2024-01-15 10:30:45] local.DEBUG: OpenFGA cache miss { "user": "user:123", "relation": "viewer", "object": "document:456" } ``` #### Performance Considerations ##### Cache Store Selection Choose an appropriate cache store for your use case: ```php // Redis - Recommended for production OPENFGA_CACHE_STORE=redis // Array - Good for testing OPENFGA_CACHE_STORE=array // File - Simple but slower OPENFGA_CACHE_STORE=file ``` ##### TTL Configuration Balance between performance and data freshness: ```php // Longer TTL for stable permissions OPENFGA_CACHE_TTL=3600 // 1 hour // Shorter TTL for frequently changing permissions OPENFGA_CACHE_TTL=60 // 1 minute // Different TTL for negative results OPENFGA_CACHE_NEGATIVE_TTL=30 // 30 seconds ``` ##### Memory Considerations For high-traffic applications, monitor cache memory usage: ```php // Limit cache entries with a shorter TTL OPENFGA_CACHE_TTL=300 // Or use cache stores with eviction policies OPENFGA_CACHE_STORE=redis ``` #### Advanced Usage ##### Custom Cache Configuration ```php use OpenFGA\Laravel\Cache\ReadThroughCache; $customCache = new ReadThroughCache($manager, [ 'enabled' => true, 'ttl' => 600, 'negative_ttl' => 120, 'error_ttl' => 5, 'prefix' => 'my-app', 'metrics_enabled' => true, ]); $allowed = $customCache->check('user:123', 'viewer', 'document:456'); ``` ##### Warming the Cache Combine with cache warming for optimal performance: ```php use OpenFGA\Laravel\Cache\CacheWarmer; $warmer = app(CacheWarmer::class); // Warm cache for specific permissions $warmer->warmForUser( 'user:123', ['viewer', 'editor'], ['document:456', 'document:789'] ); // Now these will be cache hits $allowed = $manager->check('user:123', 'viewer', 'document:456'); ``` ##### Monitoring Cache Performance ```php // In your monitoring system $stats = $manager->getReadThroughCache()->getStats(); if ($stats['hit_rate'] < 50.0) { // Alert: Low cache hit rate Log::warning('OpenFGA cache hit rate below 50%', $stats); } ``` #### Best Practices 1. **Enable metrics in production** to monitor cache effectiveness 2. **Use tagged cache stores** (Redis, DynamoDB) for better invalidation 3. **Set appropriate TTLs** based on your permission volatility 4. **Monitor cache size** to prevent memory issues 5. **Use cache warming** for predictable permission patterns 6. **Disable for tests** unless specifically testing cache behavior #### Troubleshooting ##### Low Hit Rate If your cache hit rate is low: 1. Check if permissions change frequently 2. Increase TTL if appropriate 3. Use cache warming for common permissions 4. Ensure cache store is properly configured ##### Cache Not Working 1. Verify cache is enabled: `OPENFGA_CACHE_ENABLED=true` 2. Check cache store supports your operations 3. Ensure proper permissions for file-based caches 4. Check Redis/Memcached connection if using ##### Stale Data If you're seeing outdated permissions: 1. Reduce TTL for frequently changing permissions 2. Implement proper cache invalidation on updates 3. Use contextual tuples for dynamic permissions 4. Consider disabling cache for specific checks <!-- End of evansims/openfga-laravel/docs/cache/read-through.md --> <!-- Source: evansims/openfga-laravel/docs/cache/write-behind.md --> ### Write-Behind Cache The write-behind cache pattern improves performance by buffering write operations and flushing them to OpenFGA asynchronously. This is ideal for scenarios where immediate consistency is not critical. #### Overview Write-behind caching: - Buffers grant/revoke operations in memory - Updates the read cache immediately for consistency - Flushes operations to OpenFGA in batches - Supports both synchronous and queue-based flushing #### Configuration ##### Basic Configuration ```php // config/openfga.php 'cache' => [ 'write_behind' => [ 'enabled' => env('OPENFGA_WRITE_BEHIND_ENABLED', false), 'store' => env('OPENFGA_WRITE_BEHIND_STORE'), // null = default cache 'batch_size' => env('OPENFGA_WRITE_BEHIND_BATCH_SIZE', 100), 'flush_interval' => env('OPENFGA_WRITE_BEHIND_FLUSH_INTERVAL', 5), // seconds 'ttl' => env('OPENFGA_WRITE_BEHIND_TTL', 300), // 5 minutes 'periodic_flush' => env('OPENFGA_WRITE_BEHIND_PERIODIC_FLUSH', false), 'flush_on_shutdown' => env('OPENFGA_WRITE_BEHIND_FLUSH_ON_SHUTDOWN', true), ], ], ``` ##### Queue Integration For improved reliability and scalability, enable queue-based flushing: ```php // config/openfga.php 'queue' => [ 'enabled' => env('OPENFGA_QUEUE_ENABLED', false), 'connection' => env('OPENFGA_QUEUE_CONNECTION'), // null = default 'queue' => env('OPENFGA_QUEUE_NAME', 'openfga'), ], ``` #### Usage ##### Automatic Buffering When write-behind is enabled, all write operations are automatically buffered: ```php use OpenFGA\Laravel\Facades\OpenFga; // These operations are buffered, not immediately sent to OpenFGA OpenFga::grant('user:123', 'editor', 'document:456'); OpenFga::revoke('user:456', 'viewer', 'document:456'); // The read cache is updated immediately, so this returns the correct result $canEdit = OpenFga::check('user:123', 'editor', 'document:456'); // true ``` ##### Manual Control You can manually control the write-behind cache: ```php use OpenFGA\Laravel\Cache\WriteBehindCache; $cache = app(WriteBehindCache::class); // Check pending operations $pending = $cache->getPendingCount(); // ['writes' => 5, 'deletes' => 2, 'total' => 7] // Force immediate flush $stats = $cache->flush(); // ['writes' => 5, 'deletes' => 2] // Clear pending operations without flushing $cache->clear(); ``` #### Queue-Based Flushing ##### How It Works With queue integration enabled: 1. Each operation is dispatched as a `WriteTupleToFgaJob` 2. Jobs are processed by queue workers 3. Failed jobs are automatically retried 4. Operations maintain connection context ##### Benefits - **Reliability**: Failed operations are retried automatically - **Performance**: Web requests remain fast - **Scalability**: Distribute load across multiple workers - **Fault Tolerance**: Survives OpenFGA downtime ##### Setup 1. Configure queue connection: ```env OPENFGA_QUEUE_ENABLED=true OPENFGA_QUEUE_CONNECTION=redis OPENFGA_QUEUE_NAME=openfga ``` 2. Start queue workers: ```bash php artisan queue:work --queue=openfga ``` 3. Monitor queue health: ```bash php artisan queue:monitor openfga --max=1000 ``` ##### Job Configuration The queue jobs support: - **Retries**: 3 attempts with exponential backoff (10s, 30s, 60s) - **Timeout**: 30 seconds per job - **Tags**: For monitoring in Horizon - **Connection Context**: Maintains multi-tenant context #### Flush Triggers Operations are flushed when: 1. **Batch Size Reached**: Configurable via `batch_size` 2. **Time Interval**: After `flush_interval` seconds 3. **Manual Flush**: Via `$cache->flush()` 4. **Shutdown**: If `flush_on_shutdown` is enabled 5. **Periodic**: Via scheduler if `periodic_flush` is enabled #### Monitoring ##### Artisan Commands ```bash # View write-behind cache status php artisan openfga:cache:status # Manually flush write-behind cache php artisan openfga:cache:flush # Clear write-behind cache without flushing php artisan openfga:cache:clear ``` ##### Metrics Track write-behind performance: ```php use OpenFGA\Laravel\Cache\WriteBehindCache; $cache = app(WriteBehindCache::class); $operations = $cache->getPendingOperations(); // Log metrics Log::info('Write-behind cache metrics', [ 'pending_writes' => count($operations['writes']), 'pending_deletes' => count($operations['deletes']), 'oldest_operation' => min(array_column($operations['writes'], 'timestamp')), ]); ``` ##### Laravel Horizon If using Horizon, monitor the `openfga` queue: ```php // config/horizon.php 'environments' => [ 'production' => [ 'supervisor-1' => [ 'queue' => ['default', 'openfga'], 'balance' => 'auto', 'maxProcesses' => 10, ], ], ], ``` #### Best Practices ##### 1. Use for Non-Critical Updates Write-behind is ideal for: - Bulk permission imports - Background synchronization - Non-critical permission updates Not recommended for: - Security-critical operations - Real-time permission changes - Financial or compliance systems ##### 2. Configure Appropriate Batch Sizes ```php // For high-throughput systems 'batch_size' => 500, 'flush_interval' => 2, // For low-throughput systems 'batch_size' => 50, 'flush_interval' => 10, ``` ##### 3. Monitor Queue Health Set up alerts for: - Queue size exceeding threshold - Failed job rate - Flush duration ##### 4. Handle Failures Gracefully ```php // In your exception handler public function report(Throwable $exception) { if ($exception instanceof \OpenFGA\Laravel\Exceptions\OpenFgaException) { // Notify ops team // Consider fallback strategy } parent::report($exception); } ``` ##### 5. Test with Queue Workers Always test with queue workers running: ```php // In your tests public function test_write_behind_with_queue() { Queue::fake(); config(['openfga.queue.enabled' => true]); OpenFga::grant('user:123', 'editor', 'document:456'); Queue::assertPushed(WriteTupleToFgaJob::class, function ($job) { return $job->user === 'user:123' && $job->relation === 'editor' && $job->object === 'document:456'; }); } ``` #### Troubleshooting ##### Operations Not Flushing 1. Check queue workers are running: ```bash php artisan queue:work --queue=openfga ``` 2. Verify configuration: ```bash php artisan config:cache php artisan queue:restart ``` 3. Check for failed jobs: ```bash php artisan queue:failed ``` ##### Cache Inconsistencies 1. Clear both caches: ```bash php artisan cache:clear php artisan openfga:cache:clear ``` 2. Verify write-behind is updating read cache: ```php Log::debug('Cache update', [ 'write_behind_enabled' => config('openfga.cache.write_behind.enabled'), 'read_cache_enabled' => config('openfga.cache.enabled'), ]); ``` ##### Performance Issues 1. Reduce batch size if flushes are slow 2. Increase flush interval for better batching 3. Add more queue workers for parallel processing 4. Consider using Redis for better queue performance #### Migration from Synchronous Writes To migrate from synchronous to write-behind: 1. **Enable in staging first**: ```env OPENFGA_WRITE_BEHIND_ENABLED=true OPENFGA_QUEUE_ENABLED=false ``` 2. **Monitor for issues**: - Check logs for flush failures - Verify permission consistency - Monitor response times 3. **Enable queue integration**: ```env OPENFGA_QUEUE_ENABLED=true ``` 4. **Gradually increase batch size**: - Start with small batches (50) - Increase based on performance - Monitor OpenFGA load #### See Also - [Performance Guide](https://github.com/evansims/openfga-laravel/blob/main/../performance.md) - [Queue Configuration](https://github.com/evansims/openfga-laravel/blob/main/../configuration.md#queue-configuration) - [Cache Configuration](https://github.com/evansims/openfga-laravel/blob/main/../configuration.md#cache-configuration) - [Troubleshooting](https://github.com/evansims/openfga-laravel/blob/main/../troubleshooting.md) <!-- End of evansims/openfga-laravel/docs/cache/write-behind.md --> <!-- Source: evansims/openfga-laravel/docs/cookbook/README.md --> ## Cookbook & Recipes Welcome to the OpenFGA Laravel Cookbook! This section provides practical recipes and real-world examples to help you implement common authorization patterns with OpenFGA Laravel. ### Available Recipes #### 📋 [Implementing RBAC (Role-Based Access Control)](https://github.com/evansims/openfga-laravel/blob/main/implementing-rbac.md) Learn how to implement traditional role-based access control where users are assigned roles and roles have permissions. This recipe covers: - Setting up role hierarchies - Managing user-role assignments - Role-based route protection - Conditional and temporary roles - Testing RBAC implementations **Use this when:** You need a traditional role-based system with clear role definitions and hierarchies. #### 🏢 [Handling Organization/Team Permissions](https://github.com/evansims/openfga-laravel/blob/main/organization-team-permissions.md) Implement complex organizational structures with teams, departments, and nested permissions. This recipe demonstrates: - Multi-level organizational hierarchies - Team membership with inheritance - Cross-organization collaboration - Department-based access control - Contextual access controls **Use this when:** You're building multi-tenant applications with complex organizational structures. ### Coming Soon We're working on additional recipes to cover more authorization patterns: - **Resource-Based Permissions**: Document ownership with sharing capabilities - **Attribute-Based Access Control (ABAC)**: Context-aware permissions based on attributes - **Time-Based Access Control**: Temporary permissions and scheduled access - **Geographic Access Control**: Location-based permission restrictions - **API Security Patterns**: Securing REST APIs and GraphQL endpoints - **Multi-Application SSO**: Sharing permissions across multiple applications ### How to Use These Recipes Each recipe follows this structure: 1. **Authorization Model**: The OpenFGA model definition 2. **Core Implementation**: PHP classes and services 3. **Eloquent Integration**: Laravel model integration 4. **API Endpoints**: REST API examples 5. **Testing**: Comprehensive test examples 6. **Best Practices**: Performance and security considerations ### Contributing Recipes Have a common authorization pattern you'd like to share? We welcome contributions! Please: 1. Follow the existing recipe structure 2. Include complete, working examples 3. Add comprehensive tests 4. Document any trade-offs or limitations 5. Submit a pull request ### Getting Help - Check the main [documentation](https://github.com/evansims/openfga-laravel/blob/main/../README.md) for basic concepts - Review the [API Reference](https://github.com/evansims/openfga-laravel/blob/main/../api-reference.md) for detailed method documentation - See the [Troubleshooting Guide](https://github.com/evansims/openfga-laravel/blob/main/../troubleshooting.md) for common issues - Visit our [GitHub repository](https://github.com/evansims/openfga-laravel) for support --- **Tip**: Start with the RBAC recipe if you're new to OpenFGA Laravel - it covers the fundamental concepts that apply to all other patterns. <!-- End of evansims/openfga-laravel/docs/cookbook/README.md --> <!-- Source: evansims/openfga-laravel/docs/cookbook/implementing-rbac.md --> ### Implementing RBAC (Role-Based Access Control) This recipe shows you how to implement a traditional Role-Based Access Control system using OpenFGA Laravel. RBAC is one of the most common authorization patterns, where users are assigned roles, and roles have permissions. #### Authorization Model First, define your authorization model: ```dsl model schema 1.1 type user type role relations define assignee: [user] type document relations define owner: [user] define admin: [user, role#assignee] define editor: [user, role#assignee] or admin define viewer: [user, role#assignee] or editor or owner ``` This model defines: - **Users** can be directly assigned permissions on documents - **Roles** can have users as assignees - **Documents** can have permissions granted to users directly or through roles #### Setting Up Roles ##### 1. Create Roles ```php use OpenFGA\Laravel\Facades\OpenFga; // Create organizational roles OpenFga::writeBatch([ // Marketing team roles ['role:marketing-manager', 'assignee', 'user:alice'], ['role:marketing-editor', 'assignee', 'user:bob'], ['role:marketing-viewer', 'assignee', 'user:charlie'], // Engineering team roles ['role:engineering-lead', 'assignee', 'user:david'], ['role:senior-engineer', 'assignee', 'user:eve'], ['role:junior-engineer', 'assignee', 'user:frank'], ]); ``` ##### 2. Grant Role-Based Permissions ```php // Grant permissions to roles on different document types // Marketing documents OpenFga::writeBatch([ ['role:marketing-manager', 'admin', 'document:campaign-brief'], ['role:marketing-editor', 'editor', 'document:campaign-brief'], ['role:marketing-viewer', 'viewer', 'document:campaign-brief'], ]); // Technical documents OpenFga::writeBatch([ ['role:engineering-lead', 'admin', 'document:architecture-spec'], ['role:senior-engineer', 'editor', 'document:architecture-spec'], ['role:junior-engineer', 'viewer', 'document:architecture-spec'], ]); ``` #### User Management ##### 1. Assign Users to Roles ```php class RoleManager { public function assignUserToRole(string $userId, string $roleId): bool { return OpenFga::grant("user:{$userId}", 'assignee', "role:{$roleId}"); } public function removeUserFromRole(string $userId, string $roleId): bool { return OpenFga::revoke("user:{$userId}", 'assignee', "role:{$roleId}"); } public function getUserRoles(string $userId): array { return OpenFga::listObjects("user:{$userId}", 'assignee', 'role'); } public function getRoleUsers(string $roleId): array { return OpenFga::listUsers("role:{$roleId}", 'assignee'); } } ``` ##### 2. Check Role-Based Permissions ```php class PermissionChecker { public function canUserAccessDocument(string $userId, string $permission, string $documentId): bool { return OpenFga::check("user:{$userId}", $permission, "document:{$documentId}"); } public function getUserDocuments(string $userId, string $permission): array { return OpenFga::listObjects("user:{$userId}", $permission, 'document'); } public function hasRole(string $userId, string $roleId): bool { return OpenFga::check("user:{$userId}", 'assignee', "role:{$roleId}"); } } ``` #### Eloquent Integration ##### 1. User Model ```php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use OpenFGA\Laravel\Facades\OpenFga; class User extends Authenticatable { public function assignRole(string $role): bool { return OpenFga::grant("user:{$this->id}", 'assignee', "role:{$role}"); } public function removeRole(string $role): bool { return OpenFga::revoke("user:{$this->id}", 'assignee', "role:{$role}"); } public function hasRole(string $role): bool { return OpenFga::check("user:{$this->id}", 'assignee', "role:{$role}"); } public function getRoles(): array { return OpenFga::listObjects("user:{$this->id}", 'assignee', 'role'); } public function canAccessDocument(string $documentId, string $permission = 'viewer'): bool { return OpenFga::check("user:{$this->id}", $permission, "document:{$documentId}"); } } ``` ##### 2. Document Model ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use OpenFGA\Laravel\Traits\HasAuthorization; class Document extends Model { use HasAuthorization; protected function authorizationType(): string { return 'document'; } public function grantRolePermission(string $role, string $permission): bool { return $this->grant("role:{$role}", $permission); } public function revokeRolePermission(string $role, string $permission): bool { return $this->revoke("role:{$role}", $permission); } public function getUsersWithRole(string $role, string $permission): array { // First check if the role has the permission if (!$this->check("role:{$role}", $permission)) { return []; } // Get users assigned to this role return OpenFga::listUsers("role:{$role}", 'assignee'); } } ``` #### Middleware Integration ##### 1. Role-Based Route Protection ```php // In routes/web.php // Only marketing managers can access marketing admin routes Route::middleware(['auth', 'role:marketing-manager']) ->prefix('marketing') ->group(function () { Route::get('/dashboard', [MarketingController::class, 'dashboard']); Route::post('/campaigns', [MarketingController::class, 'store']); }); // Engineering leads only Route::middleware(['auth', 'role:engineering-lead']) ->prefix('engineering') ->group(function () { Route::get('/architecture', [EngineeringController::class, 'architecture']); Route::put('/deployment', [EngineeringController::class, 'deploy']); }); ``` ##### 2. Custom Role Middleware ```php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use OpenFGA\Laravel\Facades\OpenFga; class CheckRole { public function handle(Request $request, Closure $next, string $role): mixed { $user = $request->user(); if (!$user || !OpenFga::check("user:{$user->id}", 'assignee', "role:{$role}")) { abort(403, "Access denied. Required role: {$role}"); } return $next($request); } } ``` Register the middleware: ```php // In app/Http/Kernel.php or bootstrap/app.php (Laravel 11) protected $middlewareAliases = [ 'role' => \App\Http\Middleware\CheckRole::class, ]; ``` #### Advanced RBAC Patterns ##### 1. Hierarchical Roles ```dsl type role relations define assignee: [user] define parent: [role] define inherited_assignee: assignee or parent->inherited_assignee ``` ```php // Create role hierarchy OpenFga::writeBatch([ ['role:admin', 'parent', 'role:manager'], ['role:manager', 'parent', 'role:editor'], ['role:editor', 'parent', 'role:viewer'], ]); // Grant permissions at different levels OpenFga::writeBatch([ ['role:admin', 'admin', 'document:sensitive'], ['role:manager', 'editor', 'document:sensitive'], ['role:editor', 'editor', 'document:public'], ['role:viewer', 'viewer', 'document:public'], ]); ``` ##### 2. Conditional Roles (Context-Based) ```php class ConditionalRoleChecker { public function checkWithContext(string $userId, string $permission, string $object, array $context = []): bool { // Basic role check $hasPermission = OpenFga::check("user:{$userId}", $permission, $object); if (!$hasPermission) { return false; } // Additional context checks if (isset($context['department'])) { $userDepartment = $this->getUserDepartment($userId); $objectDepartment = $this->getObjectDepartment($object); // Users can only access documents from their department if ($userDepartment !== $objectDepartment) { return false; } } if (isset($context['time_restriction'])) { // Check business hours $now = now(); if ($now->hour < 9 || $now->hour > 17) { return false; } } return true; } } ``` ##### 3. Dynamic Role Assignment ```php class DynamicRoleManager { public function assignTemporaryRole(string $userId, string $role, int $durationMinutes): void { // Grant the role OpenFga::grant("user:{$userId}", 'assignee', "role:{$role}"); // Schedule removal RevokeTemporaryRoleJob::dispatch($userId, $role) ->delay(now()->addMinutes($durationMinutes)); } public function assignProjectRole(string $userId, string $projectId, string $role): bool { // Create project-specific role $projectRole = "role:{$role}-project-{$projectId}"; return OpenFga::grant("user:{$userId}", 'assignee', $projectRole); } } ``` #### Testing RBAC ```php use OpenFGA\Laravel\Testing\FakesOpenFga; use Tests\TestCase; class RbacTest extends TestCase { use FakesOpenFga; public function test_manager_can_edit_documents() { $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); // Assign role and permissions OpenFga::grant("user:{$user->id}", 'assignee', 'role:manager'); OpenFga::grant('role:manager', 'editor', "document:{$document->id}"); $response = $this->actingAs($user) ->put("/documents/{$document->id}", [ 'title' => 'Updated Title', ]); $response->assertOk(); // Verify role-based permission was checked OpenFga::assertChecked("user:{$user->id}", 'editor', "document:{$document->id}"); } public function test_role_hierarchy_works() { $this->fakeOpenFga(); $admin = User::factory()->create(); $manager = User::factory()->create(); $viewer = User::factory()->create(); // Set up hierarchy OpenFga::grant("user:{$admin->id}", 'assignee', 'role:admin'); OpenFga::grant("user:{$manager->id}", 'assignee', 'role:manager'); OpenFga::grant("user:{$viewer->id}", 'assignee', 'role:viewer'); OpenFga::grant('role:admin', 'parent', 'role:manager'); OpenFga::grant('role:manager', 'parent', 'role:viewer'); // Admin should have all permissions $this->assertTrue($admin->hasRole('admin')); $this->assertTrue($admin->hasRole('manager')); // Through hierarchy $this->assertTrue($admin->hasRole('viewer')); // Through hierarchy // Manager should have manager and viewer permissions $this->assertFalse($manager->hasRole('admin')); $this->assertTrue($manager->hasRole('manager')); $this->assertTrue($manager->hasRole('viewer')); // Through hierarchy // Viewer should only have viewer permissions $this->assertFalse($viewer->hasRole('admin')); $this->assertFalse($viewer->hasRole('manager')); $this->assertTrue($viewer->hasRole('viewer')); } } ``` #### Best Practices ##### 1. Role Naming Conventions - Use consistent prefixes: `role:department-level` (e.g., `role:marketing-manager`) - Be descriptive: `role:content-editor` vs `role:editor` - Consider scope: `role:project-lead-mobile-app` ##### 2. Permission Granularity ```php // Too granular - hard to manage 'role:user-can-read-marketing-documents-on-weekdays' // Good balance 'role:marketing-editor' // Too broad - security risk 'role:admin-everything' ``` ##### 3. Regular Cleanup ```php class RoleCleanupCommand extends Command { public function handle() { // Remove expired temporary roles $this->cleanupExpiredRoles(); // Remove roles for inactive users $this->cleanupInactiveUserRoles(); // Audit role assignments $this->auditRoleAssignments(); } } ``` This RBAC implementation provides a solid foundation that can be extended based on your specific requirements while maintaining security and performance. <!-- End of evansims/openfga-laravel/docs/cookbook/implementing-rbac.md --> <!-- Source: evansims/openfga-laravel/docs/cookbook/organization-team-permissions.md --> ### Handling Organization/Team Permissions This recipe demonstrates how to implement complex organizational structures with teams, departments, and nested permissions. This pattern is common in enterprise applications where access control needs to reflect organizational hierarchies. #### Authorization Model ```dsl model schema 1.1 type user type organization relations define admin: [user] define member: [user] or admin type team relations define organization: [organization] define admin: [user] define member: [user] or admin define parent: [team] define member_including_parent: member or parent#member_including_parent type document relations define organization: [organization] define team: [team] define owner: [user] define admin: [user] or owner or organization#admin or team#admin define editor: [user] or admin or team#member define viewer: [user] or editor or organization#member ``` This model supports: - **Organizations** with admins and members - **Teams** within organizations with hierarchical relationships - **Documents** that can belong to organizations and teams - **Inherited permissions** from organization/team membership #### Organization Management ##### 1. Creating Organizations ```php namespace App\Services; use OpenFGA\Laravel\Facades\OpenFga; class OrganizationService { public function createOrganization(string $name, string $adminUserId): string { $organizationId = "organization:" . \Str::slug($name); // Make the creator an admin OpenFga::grant("user:{$adminUserId}", 'admin', $organizationId); return $organizationId; } public function addMember(string $organizationId, string $userId, string $role = 'member'): bool { return OpenFga::grant("user:{$userId}", $role, $organizationId); } public function removeMember(string $organizationId, string $userId, string $role = 'member'): bool { return OpenFga::revoke("user:{$userId}", $role, $organizationId); } public function getMembers(string $organizationId): array { return [ 'admins' => OpenFga::listUsers($organizationId, 'admin'), 'members' => OpenFga::listUsers($organizationId, 'member'), ]; } public function transferOwnership(string $organizationId, string $currentAdminId, string $newAdminId): bool { return OpenFga::writeBatch( writes: [ ['user:' . $newAdminId, 'admin', $organizationId], ], deletes: [ ['user:' . $currentAdminId, 'admin', $organizationId], ] ); } } ``` ##### 2. Team Management ```php class TeamService { public function createTeam(string $name, string $organizationId, string $adminUserId, ?string $parentTeamId = null): string { $teamId = "team:" . \Str::slug($name); $writes = [ // Set team admin ['user:' . $adminUserId, 'admin', $teamId], // Associate with organization [$organizationId, 'organization', $teamId], ]; // Set parent team if provided if ($parentTeamId) { $writes[] = [$parentTeamId, 'parent', $teamId]; } OpenFga::writeBatch($writes); return $teamId; } public function addTeamMember(string $teamId, string $userId, string $role = 'member'): bool { return OpenFga::grant("user:{$userId}", $role, $teamId); } public function createSubTeam(string $parentTeamId, string $name, string $adminUserId): string { $subTeamId = "team:" . \Str::slug($name); OpenFga::writeBatch([ ['user:' . $adminUserId, 'admin', $subTeamId], [$parentTeamId, 'parent', $subTeamId], ]); return $subTeamId; } public function getTeamHierarchy(string $teamId): array { $children = OpenFga::listObjects($teamId, 'parent', 'team'); $parent = OpenFga::listObjects($teamId, 'parent', 'team', inverse: true); return [ 'team_id' => $teamId, 'parent' => $parent[0] ?? null, 'children' => $children, 'members' => $this->getTeamMembers($teamId), ]; } public function getTeamMembers(string $teamId): array { return [ 'admins' => OpenFga::listUsers($teamId, 'admin'), 'members' => OpenFga::listUsers($teamId, 'member'), 'all_members' => OpenFga::listUsers($teamId, 'member_including_parent'), ]; } } ``` #### Document Management ##### 1. Document Service ```php class DocumentService { public function createDocument(array $data, string $ownerId, ?string $organizationId = null, ?string $teamId = null): Document { $document = Document::create($data); $writes = [ ['user:' . $ownerId, 'owner', "document:{$document->id}"], ]; if ($organizationId) { $writes[] = [$organizationId, 'organization', "document:{$document->id}"]; } if ($teamId) { $writes[] = [$teamId, 'team', "document:{$document->id}"]; } OpenFga::writeBatch($writes); return $document; } public function shareWithTeam(string $documentId, string $teamId, string $permission = 'viewer'): bool { return OpenFga::grant($teamId, $permission, "document:{$documentId}"); } public function shareWithOrganization(string $documentId, string $organizationId, string $permission = 'viewer'): bool { return OpenFga::grant($organizationId, $permission, "document:{$documentId}"); } public function getDocumentAccess(string $documentId): array { return [ 'owners' => OpenFga::listUsers("document:{$documentId}", 'owner'), 'admins' => OpenFga::listUsers("document:{$documentId}", 'admin'), 'editors' => OpenFga::listUsers("document:{$documentId}", 'editor'), 'viewers' => OpenFga::listUsers("document:{$documentId}", 'viewer'), 'teams' => OpenFga::listObjects("document:{$documentId}", 'team', 'team', inverse: true), 'organizations' => OpenFga::listObjects("document:{$documentId}", 'organization', 'organization', inverse: true), ]; } } ``` ##### 2. Permission Queries ```php class PermissionQueryService { public function getUserAccessibleDocuments(string $userId, string $permission = 'viewer'): array { return OpenFga::listObjects("user:{$userId}", $permission, 'document'); } public function getTeamDocuments(string $teamId, string $permission = 'viewer'): array { return OpenFga::listObjects($teamId, $permission, 'document'); } public function getOrganizationDocuments(string $organizationId, string $permission = 'viewer'): array { return OpenFga::listObjects($organizationId, $permission, 'document'); } public function canUserAccessDocument(string $userId, string $documentId, string $permission = 'viewer'): bool { return OpenFga::check("user:{$userId}", $permission, "document:{$documentId}"); } public function getUserPermissionsOnDocument(string $userId, string $documentId): array { $permissions = []; foreach (['owner', 'admin', 'editor', 'viewer'] as $permission) { if (OpenFga::check("user:{$userId}", $permission, "document:{$documentId}")) { $permissions[] = $permission; } } return $permissions; } } ``` #### Eloquent Integration ##### 1. Organization Model ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use OpenFGA\Laravel\Facades\OpenFga; class Organization extends Model { protected $fillable = ['name', 'slug', 'description']; public function teams(): HasMany { return $this->hasMany(Team::class); } public function documents(): HasMany { return $this->hasMany(Document::class); } public function addMember(User $user, string $role = 'member'): bool { return OpenFga::grant("user:{$user->id}", $role, "organization:{$this->id}"); } public function removeMember(User $user, string $role = 'member'): bool { return OpenFga::revoke("user:{$user->id}", $role, "organization:{$this->id}"); } public function isMember(User $user): bool { return OpenFga::check("user:{$user->id}", 'member', "organization:{$this->id}"); } public function isAdmin(User $user): bool { return OpenFga::check("user:{$user->id}", 'admin', "organization:{$this->id}"); } public function getMembers(): array { return [ 'admins' => OpenFga::listUsers("organization:{$this->id}", 'admin'), 'members' => OpenFga::listUsers("organization:{$this->id}", 'member'), ]; } } ``` ##### 2. Team Model ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use OpenFGA\Laravel\Facades\OpenFga; class Team extends Model { protected $fillable = ['name', 'slug', 'description', 'organization_id', 'parent_id']; public function organization(): BelongsTo { return $this->belongsTo(Organization::class); } public function parent(): BelongsTo { return $this->belongsTo(Team::class, 'parent_id'); } public function children(): HasMany { return $this->hasMany(Team::class, 'parent_id'); } public function documents(): HasMany { return $this->hasMany(Document::class); } public function addMember(User $user, string $role = 'member'): bool { return OpenFga::grant("user:{$user->id}", $role, "team:{$this->id}"); } public function removeMember(User $user, string $role = 'member'): bool { return OpenFga::revoke("user:{$user->id}", $role, "team:{$this->id}"); } public function isMember(User $user, bool $includeParent = false): bool { $relation = $includeParent ? 'member_including_parent' : 'member'; return OpenFga::check("user:{$user->id}", $relation, "team:{$this->id}"); } public function isAdmin(User $user): bool { return OpenFga::check("user:{$user->id}", 'admin', "team:{$this->id}"); } public function getMembers(bool $includeParent = false): array { $relation = $includeParent ? 'member_including_parent' : 'member'; return [ 'admins' => OpenFga::listUsers("team:{$this->id}", 'admin'), 'members' => OpenFga::listUsers("team:{$this->id}", $relation), ]; } } ``` ##### 3. Enhanced Document Model ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use OpenFGA\Laravel\Traits\HasAuthorization; class Document extends Model { use HasAuthorization; protected $fillable = ['title', 'content', 'organization_id', 'team_id', 'user_id']; public function organization(): BelongsTo { return $this->belongsTo(Organization::class); } public function team(): BelongsTo { return $this->belongsTo(Team::class); } public function owner(): BelongsTo { return $this->belongsTo(User::class, 'user_id'); } protected function authorizationType(): string { return 'document'; } public function shareWithTeam(Team $team, string $permission = 'viewer'): bool { return $this->grant("team:{$team->id}", $permission); } public function shareWithOrganization(Organization $organization, string $permission = 'viewer'): bool { return $this->grant("organization:{$organization->id}", $permission); } public function getTeamAccess(): array { return OpenFga::listObjects($this->authorizationObject(), 'team', 'team', inverse: true); } public function getOrganizationAccess(): array { return OpenFga::listObjects($this->authorizationObject(), 'organization', 'organization', inverse: true); } } ``` #### Advanced Patterns ##### 1. Cross-Organization Collaboration ```php class CollaborationService { public function createCrossOrgProject(array $organizationIds, string $projectName, string $creatorId): string { $projectId = "project:" . \Str::slug($projectName); $writes = [ ['user:' . $creatorId, 'admin', $projectId], ]; // Add all organizations as members foreach ($organizationIds as $orgId) { $writes[] = [$orgId, 'member', $projectId]; } OpenFga::writeBatch($writes); return $projectId; } public function inviteExternalUser(string $projectId, string $email, string $role = 'viewer'): bool { // Create temporary user for external invite $tempUserId = "temp-user:" . md5($email); return OpenFga::grant($tempUserId, $role, $projectId); } } ``` ##### 2. Department-Based Access Control ```php class DepartmentService { public function setupDepartmentStructure(string $organizationId): void { $departments = [ 'engineering' => ['backend-team', 'frontend-team', 'devops-team'], 'marketing' => ['content-team', 'social-media-team', 'analytics-team'], 'sales' => ['inside-sales-team', 'field-sales-team', 'sales-ops-team'], ]; foreach ($departments as $deptName => $teams) { $deptId = "team:{$deptName}"; // Create department OpenFga::grant($organizationId, 'organization', $deptId); // Create teams under department foreach ($teams as $teamName) { $teamId = "team:{$teamName}"; OpenFga::writeBatch([ [$organizationId, 'organization', $teamId], [$deptId, 'parent', $teamId], ]); } } } public function getDepartmentMembers(string $departmentId): array { return OpenFga::listUsers($departmentId, 'member_including_parent'); } } ``` ##### 3. Conditional Access Based on Context ```php class ContextualAccessService { public function checkConditionalAccess(string $userId, string $documentId, array $context = []): bool { // Basic permission check if (!OpenFga::check("user:{$userId}", 'viewer', "document:{$documentId}")) { return false; } // Location-based access if (isset($context['location'])) { if (!$this->isLocationAllowed($userId, $context['location'])) { return false; } } // Time-based access if (isset($context['time_restriction'])) { if (!$this->isTimeAllowed($userId, $context['time_restriction'])) { return false; } } // Project-based access if (isset($context['project'])) { if (!$this->isProjectMember($userId, $context['project'])) { return false; } } return true; } private function isLocationAllowed(string $userId, string $location): bool { // Check if user is in allowed location $allowedLocations = OpenFga::listObjects("user:{$userId}", 'allowed_location', 'location'); return in_array($location, $allowedLocations); } private function isTimeAllowed(string $userId, string $timeRestriction): bool { // Check business hours, etc. return match($timeRestriction) { 'business_hours' => now()->between('09:00', '17:00'), 'extended_hours' => now()->between('07:00', '22:00'), 'always' => true, default => false, }; } private function isProjectMember(string $userId, string $projectId): bool { return OpenFga::check("user:{$userId}", 'member', "project:{$projectId}"); } } ``` #### API Endpoints ##### 1. Organization Management API ```php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Organization; use App\Services\OrganizationService; use Illuminate\Http\Request; class OrganizationController extends Controller { public function __construct(private OrganizationService $organizationService) { } public function store(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'description' => 'nullable|string', ]); $organization = Organization::create($request->validated()); // Set up OpenFGA permissions $this->organizationService->createOrganization( $organization->name, $request->user()->id ); return response()->json($organization, 201); } public function addMember(Request $request, Organization $organization) { $this->authorize('admin', $organization); $request->validate([ 'user_id' => 'required|exists:users,id', 'role' => 'required|in:admin,member', ]); $success = $this->organizationService->addMember( "organization:{$organization->id}", $request->user_id, $request->role ); return response()->json(['success' => $success]); } public function members(Organization $organization) { $this->authorize('member', $organization); $members = $this->organizationService->getMembers("organization:{$organization->id}"); return response()->json($members); } } ``` ##### 2. Team Management API ```php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Team; use App\Services\TeamService; use Illuminate\Http\Request; class TeamController extends Controller { public function __construct(private TeamService $teamService) { } public function store(Request $request) { $request->validate([ 'name' => 'required|string|max:255', 'organization_id' => 'required|exists:organizations,id', 'parent_id' => 'nullable|exists:teams,id', ]); $this->authorize('admin', Organization::find($request->organization_id)); $team = Team::create($request->validated()); // Set up OpenFGA permissions $this->teamService->createTeam( $team->name, "organization:{$team->organization_id}", $request->user()->id, $request->parent_id ? "team:{$request->parent_id}" : null ); return response()->json($team, 201); } public function hierarchy(Team $team) { $this->authorize('member', $team); $hierarchy = $this->teamService->getTeamHierarchy("team:{$team->id}"); return response()->json($hierarchy); } } ``` #### Testing ```php use OpenFGA\Laravel\Testing\FakesOpenFga; use Tests\TestCase; class OrganizationTeamTest extends TestCase { use FakesOpenFga; public function test_organization_member_can_access_team_documents() { $this->fakeOpenFga(); $user = User::factory()->create(); $organization = Organization::factory()->create(); $team = Team::factory()->create(['organization_id' => $organization->id]); $document = Document::factory()->create(['team_id' => $team->id]); // Set up permissions OpenFga::grant("user:{$user->id}", 'member', "organization:{$organization->id}"); OpenFga::grant("organization:{$organization->id}", 'organization', "team:{$team->id}"); OpenFga::grant("team:{$team->id}", 'team', "document:{$document->id}"); // Test access $this->assertTrue( OpenFga::check("user:{$user->id}", 'viewer', "document:{$document->id}") ); OpenFga::assertChecked("user:{$user->id}", 'viewer', "document:{$document->id}"); } public function test_team_hierarchy_permissions() { $this->fakeOpenFga(); $user = User::factory()->create(); $parentTeam = Team::factory()->create(); $childTeam = Team::factory()->create(); // Set up hierarchy OpenFga::grant("user:{$user->id}", 'member', "team:{$parentTeam->id}"); OpenFga::grant("team:{$parentTeam->id}", 'parent', "team:{$childTeam->id}"); // User should be member of child team through parent $this->assertTrue( OpenFga::check("user:{$user->id}", 'member_including_parent', "team:{$childTeam->id}") ); } } ``` This comprehensive approach to organization and team permissions provides a flexible foundation that can be adapted to various organizational structures while maintaining security and performance. <!-- End of evansims/openfga-laravel/docs/cookbook/organization-team-permissions.md --> <!-- Source: evansims/openfga-laravel/docs/api-reference.md --> ### API Reference This document provides a complete reference for all public methods and classes in the OpenFGA Laravel package. #### Table of Contents - [OpenFgaManager](#openfgamanager) - [Facades](#facades) - [Traits](#traits) - [Middleware](#middleware) - [Commands](#commands) - [Events](#events) - [Exceptions](#exceptions) - [Testing](#testing) #### OpenFgaManager The main service class for interacting with OpenFGA. ##### Methods ###### check() Check if a user has a specific permission on an object. ```php public function check( string $user, string $relation, string $object, ?array $contextualTuples = null, ?array $context = null, ?string $connection = null ): bool ``` **Parameters:** - `$user` - User identifier (e.g., 'user:123' or '@me') - `$relation` - The relation to check (e.g., 'viewer', 'editor') - `$object` - Object identifier (e.g., 'document:456') - `$contextualTuples` - Optional contextual tuples for the check - `$context` - Optional context data - `$connection` - Optional connection name **Returns:** `bool` - Whether the user has the permission **Example:** ```php $allowed = OpenFga::check('user:123', 'editor', 'document:456'); ``` ###### grant() Grant a permission to a user. ```php public function grant( string $user, string $relation, string $object, ?string $connection = null ): void ``` **Parameters:** - `$user` - User identifier - `$relation` - The relation to grant - `$object` - Object identifier - `$connection` - Optional connection name **Example:** ```php OpenFga::grant('user:123', 'editor', 'document:456'); ``` ###### revoke() Revoke a permission from a user. ```php public function revoke( string $user, string $relation, string $object, ?string $connection = null ): void ``` **Parameters:** - `$user` - User identifier - `$relation` - The relation to revoke - `$object` - Object identifier - `$connection` - Optional connection name **Example:** ```php OpenFga::revoke('user:123', 'editor', 'document:456'); ``` ###### writeBatch() Perform batch write operations. ```php public function writeBatch( array $writes = [], array $deletes = [], ?string $connection = null ): void ``` **Parameters:** - `$writes` - Array of tuples to write `[user, relation, object]` - `$deletes` - Array of tuples to delete `[user, relation, object]` - `$connection` - Optional connection name **Example:** ```php OpenFga::writeBatch( writes: [ ['user:123', 'viewer', 'document:456'], ['user:123', 'editor', 'document:789'], ], deletes: [ ['user:456', 'viewer', 'document:456'], ] ); ``` ###### batchCheck() Check multiple permissions in one request. ```php public function batchCheck( array $checks, ?string $connection = null ): array ``` **Parameters:** - `$checks` - Array of checks `[user, relation, object]` - `$connection` - Optional connection name **Returns:** `array` - Array of boolean results **Example:** ```php $results = OpenFga::batchCheck([ ['user:123', 'viewer', 'document:456'], ['user:123', 'editor', 'document:456'], ]); // Returns: [true, false] ``` ###### expand() Expand a relation to see all users who have access. ```php public function expand( string $object, string $relation, ?string $connection = null ): array ``` **Parameters:** - `$object` - Object identifier - `$relation` - The relation to expand - `$connection` - Optional connection name **Returns:** `array` - Expansion result **Example:** ```php $result = OpenFga::expand('document:456', 'viewer'); ``` ###### listObjects() List all objects a user has a specific permission for. ```php public function listObjects( string $user, string $relation, string $type, ?array $contextualTuples = null, ?string $connection = null ): array ``` **Parameters:** - `$user` - User identifier - `$relation` - The relation to check - `$type` - Object type to filter - `$contextualTuples` - Optional contextual tuples - `$connection` - Optional connection name **Returns:** `array` - Array of object identifiers **Example:** ```php $documents = OpenFga::listObjects('user:123', 'viewer', 'document'); // Returns: ['document:456', 'document:789'] ``` ###### query() Create a fluent query builder instance. ```php public function query(?string $connection = null): AuthorizationQuery ``` **Parameters:** - `$connection` - Optional connection name **Returns:** `AuthorizationQuery` - Query builder instance **Example:** ```php $result = OpenFga::query() ->for('user:123') ->can('edit') ->on('document:456') ->check(); ``` ###### connection() Switch to a different connection. ```php public function connection(?string $name = null): self ``` **Parameters:** - `$name` - Connection name (null for default) **Returns:** `self` - Manager instance using specified connection **Example:** ```php OpenFga::connection('secondary')->check($user, $relation, $object); ``` #### Facades ##### OpenFga Facade Provides static access to the OpenFgaManager. ```php use OpenFGA\Laravel\Facades\OpenFga; // All OpenFgaManager methods are available OpenFga::check('@me', 'viewer', 'document:123'); OpenFga::grant('user:456', 'editor', 'document:123'); ``` #### Traits ##### HasAuthorization Add authorization capabilities to Eloquent models. ###### Methods ###### grant() ```php public function grant( Model|string $user, string $relation ): void ``` **Example:** ```php $document->grant($user, 'editor'); $document->grant('user:123', 'viewer'); ``` ###### check() ```php public function check( Model|string $user, string $relation ): bool ``` **Example:** ```php if ($document->check($user, 'editor')) { // User can edit } ``` ###### revoke() ```php public function revoke( Model|string $user, string $relation ): void ``` **Example:** ```php $document->revoke($user, 'editor'); ``` ###### revokeAll() ```php public function revokeAll(Model|string $user): void ``` **Example:** ```php $document->revokeAll($user); ``` ###### revokeAllPermissions() ```php public function revokeAllPermissions(): void ``` **Example:** ```php $document->revokeAllPermissions(); ``` ###### getUsersWithPermission() ```php public function getUsersWithPermission(string $relation): array ``` **Example:** ```php $editors = $document->getUsersWithPermission('editor'); ``` ###### authorizationObject() ```php public function authorizationObject(): string ``` **Example:** ```php $objectId = $document->authorizationObject(); // Returns: "document:123" ``` ###### authorizationType() ```php protected function authorizationType(): string ``` Override to customize the object type. **Example:** ```php protected function authorizationType(): string { return 'doc'; // Results in "doc:123" } ``` ###### Scopes ###### whereUserCan() ```php public function scopeWhereUserCan( Builder $query, Model|string $user, string $relation ): Builder ``` **Example:** ```php $documents = Document::whereUserCan($user, 'viewer')->get(); ``` ###### whereUserCanAny() ```php public function scopeWhereUserCanAny( Builder $query, Model|string $user, array $relations ): Builder ``` **Example:** ```php $documents = Document::whereUserCanAny($user, ['viewer', 'editor'])->get(); ``` ###### whereUserCanAll() ```php public function scopeWhereUserCanAll( Builder $query, Model|string $user, array $relations ): Builder ``` **Example:** ```php $documents = Document::whereUserCanAll($user, ['viewer', 'commenter'])->get(); ``` ##### AuthorizesWithOpenFga Add OpenFGA authorization to Form Requests. ###### Methods ###### checkPermission() ```php protected function checkPermission( string $relation, Model|string $object ): bool ``` **Example:** ```php public function authorize(): bool { return $this->checkPermission('editor', $this->route('document')); } ``` ###### checkAnyPermission() ```php protected function checkAnyPermission( array $relations, Model|string $object ): bool ``` **Example:** ```php public function authorize(): bool { return $this->checkAnyPermission(['editor', 'owner'], $this->route('document')); } ``` ###### checkAllPermissions() ```php protected function checkAllPermissions( array $relations, Model|string $object ): bool ``` **Example:** ```php public function authorize(): bool { return $this->checkAllPermissions(['member', 'active'], $this->route('team')); } ``` ##### FakesOpenFga Testing trait for faking OpenFGA operations. ###### Methods ###### fakeOpenFga() ```php protected function fakeOpenFga(): void ``` **Example:** ```php protected function setUp(): void { parent::setUp(); $this->fakeOpenFga(); } ``` #### Middleware ##### OpenFgaMiddleware Base middleware for route protection. ```php Route::middleware(['openfga:editor,document:{document}']) ->get('/documents/{document}/edit', [DocumentController::class, 'edit']); ``` **Format:** `openfga:relation,object[,connection]` ##### RequiresAnyPermission Check if user has any of the specified permissions. ```php Route::middleware(['can.any:viewer,editor,owner,document:{document}']) ->get('/documents/{document}', [DocumentController::class, 'show']); ``` **Format:** `can.any:relation1,relation2,...,object[,connection]` ##### RequiresAllPermissions Check if user has all specified permissions. ```php Route::middleware(['can.all:member,billing,team:{team}']) ->get('/teams/{team}/billing', [BillingController::class, 'index']); ``` **Format:** `can.all:relation1,relation2,...,object[,connection]` #### Commands ##### openfga:check Check if a user has permission. ```bash php artisan openfga:check {user} {relation} {object} [options] ``` **Options:** - `--connection[=CONNECTION]` - The connection to use - `--json` - Output as JSON - `--contextual-tuple[=CONTEXTUAL-TUPLE]` - Contextual tuples (multiple allowed) - `--context[=CONTEXT]` - Context values as key=value (multiple allowed) **Example:** ```bash php artisan openfga:check user:123 editor document:456 php artisan openfga:check user:123 viewer document:456 --json ``` ##### openfga:grant Grant permission to a user. ```bash php artisan openfga:grant {user} {relation} {object} [options] ``` **Options:** - `--connection[=CONNECTION]` - The connection to use - `--batch` - Enable batch mode for multiple users **Example:** ```bash php artisan openfga:grant user:123 editor document:456 php artisan openfga:grant user:123,user:456,user:789 viewer document:456 --batch ``` ##### openfga:revoke Revoke permission from a user. ```bash php artisan openfga:revoke {user} {relation} {object} [options] ``` **Options:** - `--connection[=CONNECTION]` - The connection to use - `--all` - Revoke all permissions for the user on this object **Example:** ```bash php artisan openfga:revoke user:123 editor document:456 php artisan openfga:revoke user:123 all document:456 --all ``` ##### openfga:expand Expand a relation to see all users. ```bash php artisan openfga:expand {object} {relation} [options] ``` **Options:** - `--connection[=CONNECTION]` - The connection to use - `--json` - Output as JSON **Example:** ```bash php artisan openfga:expand document:456 viewer php artisan openfga:expand document:456 editor --json ``` ##### openfga:list-objects List objects a user has permission for. ```bash php artisan openfga:list-objects {user} {relation} {type} [options] ``` **Options:** - `--connection[=CONNECTION]` - The connection to use - `--json` - Output as JSON **Example:** ```bash php artisan openfga:list-objects user:123 viewer document php artisan openfga:list-objects user:123 editor folder --json ``` ##### openfga:debug Debug OpenFGA configuration and connection. ```bash php artisan openfga:debug [options] ``` **Options:** - `--connection[=CONNECTION]` - Test specific connection **Example:** ```bash php artisan openfga:debug php artisan openfga:debug --connection=secondary ``` ##### openfga:stats Display permission statistics. ```bash php artisan openfga:stats [options] ``` **Options:** - `--days[=DAYS]` - Number of days to show (default: 7) - `--json` - Output as JSON **Example:** ```bash php artisan openfga:stats php artisan openfga:stats --days=30 ``` #### Events ##### PermissionChecked Fired when a permission is checked. ```php namespace OpenFGA\Laravel\Events; class PermissionChecked { public string $user; public string $relation; public string $object; public bool $allowed; public ?string $connection; public float $duration; public bool $cached; public array $context; } ``` ##### PermissionGranted Fired when a permission is granted. ```php namespace OpenFGA\Laravel\Events; class PermissionGranted { public string $user; public string $relation; public string $object; public ?string $connection; public float $duration; } ``` ##### PermissionRevoked Fired when a permission is revoked. ```php namespace OpenFGA\Laravel\Events; class PermissionRevoked { public string $user; public string $relation; public string $object; public ?string $connection; public float $duration; } ``` ##### BatchWriteCompleted Fired when a batch write completes. ```php namespace OpenFGA\Laravel\Events; class BatchWriteCompleted { public array $writes; public array $deletes; public ?string $connection; public float $duration; public array $options; } ``` ##### BatchWriteFailed Fired when a batch write fails. ```php namespace OpenFGA\Laravel\Events; class BatchWriteFailed { public array $writes; public array $deletes; public ?string $connection; public Throwable $exception; public array $options; } ``` #### Exceptions ##### AuthorizationException Thrown when authorization operations fail. ```php namespace OpenFGA\Laravel\Exceptions; class AuthorizationException extends Exception { public function getUser(): ?string; public function getRelation(): ?string; public function getObject(): ?string; } ``` ##### ConfigurationException Thrown when configuration is invalid. ```php namespace OpenFGA\Laravel\Exceptions; class ConfigurationException extends Exception { public function getConnection(): ?string; public function getConfigKey(): ?string; } ``` #### Testing ##### FakeOpenFga Fake implementation for testing. ###### Assertion Methods ###### assertGranted() ```php OpenFga::assertGranted(string $user, string $relation, string $object): void ``` **Example:** ```php OpenFga::assertGranted('user:123', 'editor', 'document:456'); ``` ###### assertRevoked() ```php OpenFga::assertRevoked(string $user, string $relation, string $object): void ``` **Example:** ```php OpenFga::assertRevoked('user:123', 'editor', 'document:456'); ``` ###### assertChecked() ```php OpenFga::assertChecked(string $user, string $relation, string $object): void ``` **Example:** ```php OpenFga::assertChecked('user:123', 'viewer', 'document:456'); ``` ###### assertNotChecked() ```php OpenFga::assertNotChecked(string $user, string $relation, string $object): void ``` **Example:** ```php OpenFga::assertNotChecked('user:123', 'admin', 'system:core'); ``` ###### assertNothingGranted() ```php OpenFga::assertNothingGranted(): void ``` **Example:** ```php OpenFga::assertNothingGranted(); ``` ###### assertBatchWritten() ```php OpenFga::assertBatchWritten(array $expectedWrites, array $expectedDeletes = []): void ``` **Example:** ```php OpenFga::assertBatchWritten([ ['user:123', 'viewer', 'document:456'], ['user:456', 'editor', 'document:456'], ]); ``` ###### Mocking Methods ###### shouldCheck() ```php OpenFga::shouldCheck( string $user, string $relation, string $object, bool $result ): void ``` **Example:** ```php OpenFga::shouldCheck('user:123', 'editor', 'document:456', true); ``` ###### shouldListObjects() ```php OpenFga::shouldListObjects( string $user, string $relation, string $type, array $objects ): void ``` **Example:** ```php OpenFga::shouldListObjects( 'user:123', 'viewer', 'document', ['document:456', 'document:789'] ); ``` ###### shouldFail() ```php OpenFga::shouldFail(string $message = 'Operation failed'): void ``` **Example:** ```php OpenFga::shouldFail('Connection timeout'); ``` ###### shouldFailTimes() ```php OpenFga::shouldFailTimes(int $times, string $message = 'Operation failed'): void ``` **Example:** ```php OpenFga::shouldFailTimes(2, 'Service unavailable'); ``` #### Configuration ##### Connection Configuration ```php 'connections' => [ 'main' => [ 'url' => string, // OpenFGA server URL 'store_id' => string, // Store identifier 'model_id' => ?string, // Model ID (null for latest) 'credentials' => [ 'method' => string, // 'none', 'api_token', 'client_credentials' 'token' => ?string, // API token 'client_id' => ?string, // OAuth client ID 'client_secret' => ?string, // OAuth client secret 'api_token_issuer' => ?string, 'api_audience' => ?string, 'scopes' => array, // OAuth scopes ], 'retries' => [ 'max_retries' => int, // Max retry attempts 'min_wait_ms' => int, // Min wait between retries ], 'http_options' => [ 'timeout' => int, // Request timeout 'connect_timeout' => int, // Connection timeout ], ], ], ``` ##### Cache Configuration ```php 'cache' => [ 'enabled' => bool, // Enable caching 'store' => string, // Cache store to use 'ttl' => int, // TTL in seconds 'prefix' => string, // Cache key prefix ], ``` ##### Queue Configuration ```php 'queue' => [ 'enabled' => bool, // Enable queue support 'connection' => string, // Queue connection 'queue' => string, // Queue name ], ``` #### Blade Directives ##### @can ```blade @can('editor', 'document:' . $document->id) <button>Edit Document</button> @endcan ``` ##### @cannot ```blade @cannot('owner', 'document:' . $document->id) <p>You don't own this document</p> @endcannot ``` ##### @canany ```blade @canany(['editor', 'owner'], 'document:' . $document->id) <button>Manage Document</button> @endcanany ``` ##### @canall ```blade @canall(['member', 'verified'], 'team:' . $team->id) <div>Premium Team Features</div> @endcanall ``` #### Helper Functions ##### can() ```php function can(string $relation, string $object): bool ``` **Example:** ```php if (can('editor', 'document:123')) { // Current user can edit } ``` ##### cannot() ```php function cannot(string $relation, string $object): bool ``` **Example:** ```php if (cannot('owner', 'document:123')) { // Current user is not owner } ``` ##### canAny() ```php function canAny(array $relations, string $object): bool ``` **Example:** ```php if (canAny(['viewer', 'editor'], 'document:123')) { // User has at least one permission } ``` ##### canAll() ```php function canAll(array $relations, string $object): bool ``` **Example:** ```php if (canAll(['member', 'active'], 'team:123')) { // User has all permissions } ``` <!-- End of evansims/openfga-laravel/docs/api-reference.md --> <!-- Source: evansims/openfga-laravel/docs/configuration.md --> ### Configuration Guide This guide covers all configuration options available in the OpenFGA Laravel package. The configuration file is located at `config/openfga.php` after publishing. #### Configuration File Structure ```php <?php return [ 'default' => env('OPENFGA_CONNECTION', 'main'), 'connections' => [ // Connection configurations ], 'cache' => [ // Cache settings ], 'queue' => [ // Queue settings ], 'logging' => [ // Logging settings ], 'options' => [ // Global options ], ]; ``` #### Connection Configuration ##### Single Connection For most applications, a single connection is sufficient: ```php 'connections' => [ 'main' => [ 'url' => env('OPENFGA_URL', 'http://localhost:8080'), 'store_id' => env('OPENFGA_STORE_ID'), 'model_id' => env('OPENFGA_MODEL_ID'), 'credentials' => [ 'method' => env('OPENFGA_AUTH_METHOD', 'none'), 'token' => env('OPENFGA_API_TOKEN'), 'client_id' => env('OPENFGA_CLIENT_ID'), 'client_secret' => env('OPENFGA_CLIENT_SECRET'), 'api_token_issuer' => env('OPENFGA_TOKEN_ISSUER'), 'api_audience' => env('OPENFGA_API_AUDIENCE'), 'scopes' => explode(',', env('OPENFGA_SCOPES', '')), ], 'retries' => [ 'max_retries' => 3, 'min_wait_ms' => 100, ], 'http_options' => [ 'timeout' => 30, 'connect_timeout' => 10, ], ], ], ``` ##### Multiple Connections Configure multiple connections for different environments or stores: ```php 'connections' => [ 'production' => [ 'url' => env('OPENFGA_PROD_URL'), 'store_id' => env('OPENFGA_PROD_STORE_ID'), 'model_id' => env('OPENFGA_PROD_MODEL_ID'), // ... other settings ], 'staging' => [ 'url' => env('OPENFGA_STAGING_URL'), 'store_id' => env('OPENFGA_STAGING_STORE_ID'), 'model_id' => env('OPENFGA_STAGING_MODEL_ID'), // ... other settings ], 'development' => [ 'url' => 'http://localhost:8080', 'store_id' => 'dev-store', 'model_id' => null, // Uses latest model // ... other settings ], ], ``` Using different connections: ```php use OpenFGA\Laravel\Facades\OpenFga; // Use default connection OpenFga::check('user:123', 'reader', 'document:456'); // Use specific connection OpenFga::connection('staging')->check('user:123', 'reader', 'document:456'); // Switch default connection OpenFga::setDefaultConnection('production'); ``` #### Authentication Methods ##### No Authentication For local development or unsecured environments: ```php 'credentials' => [ 'method' => 'none', ], ``` ##### API Token For token-based authentication: ```php 'credentials' => [ 'method' => 'api_token', 'token' => env('OPENFGA_API_TOKEN'), ], ``` ##### OAuth2 Client Credentials For OAuth2 authentication: ```php 'credentials' => [ 'method' => 'client_credentials', 'client_id' => env('OPENFGA_CLIENT_ID'), 'client_secret' => env('OPENFGA_CLIENT_SECRET'), 'api_token_issuer' => env('OPENFGA_TOKEN_ISSUER'), 'api_audience' => env('OPENFGA_API_AUDIENCE'), 'scopes' => ['read', 'write', 'admin'], ], ``` #### Retry Configuration Configure retry behavior for failed requests: ```php 'retries' => [ 'max_retries' => 3, // Maximum number of retry attempts 'min_wait_ms' => 100, // Minimum wait time between retries 'max_wait_ms' => 5000, // Maximum wait time between retries 'retry_on' => [ // HTTP status codes to retry on 429, // Too Many Requests 500, // Internal Server Error 502, // Bad Gateway 503, // Service Unavailable 504, // Gateway Timeout ], ], ``` #### HTTP Options Configure HTTP client options: ```php 'http_options' => [ 'timeout' => 30, // Request timeout in seconds 'connect_timeout' => 10, // Connection timeout in seconds 'proxy' => env('HTTP_PROXY'), 'verify' => true, // Verify SSL certificates 'headers' => [ 'User-Agent' => 'OpenFGA-Laravel/1.0', 'X-Custom-Header' => 'value', ], ], ``` #### Cache Configuration ##### Basic Cache Settings ```php 'cache' => [ 'enabled' => env('OPENFGA_CACHE_ENABLED', true), 'store' => env('OPENFGA_CACHE_STORE', 'default'), 'ttl' => env('OPENFGA_CACHE_TTL', 300), // 5 minutes 'prefix' => 'openfga', ], ``` ##### Advanced Cache Configuration ```php 'cache' => [ 'enabled' => true, 'store' => 'redis', 'ttl' => 300, 'prefix' => 'openfga', // Cache specific operations 'operations' => [ 'check' => true, // Cache permission checks 'list_objects' => true, // Cache object listings 'expand' => false, // Don't cache expansions ], // Cache tags for invalidation 'tags' => [ 'enabled' => true, 'prefix' => 'openfga-tags', ], // Warm cache on specific events 'warming' => [ 'enabled' => true, 'on_grant' => true, // Warm cache when granting permissions 'on_revoke' => false, // Don't warm cache on revoke ], ], ``` #### Queue Configuration ##### Basic Queue Settings ```php 'queue' => [ 'enabled' => env('OPENFGA_QUEUE_ENABLED', false), 'connection' => env('OPENFGA_QUEUE_CONNECTION', 'default'), 'queue' => env('OPENFGA_QUEUE_NAME', 'openfga'), ], ``` ##### Advanced Queue Configuration ```php 'queue' => [ 'enabled' => true, 'connection' => 'redis', 'queue' => 'openfga', // Batch settings 'batch' => [ 'size' => 100, // Max operations per batch 'delay' => 0, // Delay in seconds 'tries' => 3, // Retry attempts 'timeout' => 120, // Job timeout ], // Auto-queue large operations 'auto_queue' => [ 'enabled' => true, 'threshold' => 50, // Queue if more than 50 operations ], ], ``` #### Logging Configuration ##### Basic Logging ```php 'logging' => [ 'enabled' => env('OPENFGA_LOGGING_ENABLED', true), 'channel' => env('OPENFGA_LOG_CHANNEL', 'default'), ], ``` ##### Advanced Logging ```php 'logging' => [ 'enabled' => true, 'channel' => 'openfga', // Log levels for different operations 'levels' => [ 'check' => 'debug', 'write' => 'info', 'error' => 'error', 'performance' => 'info', ], // Performance logging 'performance' => [ 'enabled' => true, 'threshold' => 100, // Log requests taking > 100ms ], // Sensitive data handling 'redact' => [ 'user_ids' => false, // Don't redact user IDs 'object_ids' => false, // Don't redact object IDs 'context' => true, // Redact context data ], ], ``` #### Global Options ##### Exception Handling ```php 'options' => [ 'throw_on_error' => env('OPENFGA_THROW_ON_ERROR', true), 'exception_class' => \OpenFGA\Laravel\Exceptions\AuthorizationException::class, ], ``` ##### Model Cleanup ```php 'options' => [ 'cleanup_on_delete' => true, // Auto-revoke permissions when models are deleted 'cleanup_queue' => true, // Use queue for cleanup operations ], ``` ##### Performance Options ```php 'options' => [ 'batch_size' => 100, // Default batch size for operations 'connection_pooling' => true, // Enable connection pooling 'persistent_connections' => false, ], ``` #### Environment-Specific Configuration ##### Using Environment Files ```env # Production OPENFGA_URL=https://api.openfga.example.com OPENFGA_STORE_ID=prod-store-id OPENFGA_MODEL_ID=prod-model-id OPENFGA_CACHE_ENABLED=true OPENFGA_QUEUE_ENABLED=true OPENFGA_LOGGING_ENABLED=false # Development OPENFGA_URL=http://localhost:8080 OPENFGA_STORE_ID=dev-store-id OPENFGA_MODEL_ID= OPENFGA_CACHE_ENABLED=false OPENFGA_QUEUE_ENABLED=false OPENFGA_LOGGING_ENABLED=true ``` ##### Dynamic Configuration ```php // In AppServiceProvider use OpenFGA\Laravel\Facades\OpenFga; public function boot() { if ($this->app->environment('production')) { config(['openfga.cache.ttl' => 600]); // 10 minutes in production config(['openfga.queue.enabled' => true]); } if ($this->app->environment('local')) { config(['openfga.logging.levels.check' => 'debug']); } } ``` #### Configuration Validation The package validates configuration on boot. Common validation errors: ##### Invalid Store ID ``` OpenFGA Configuration Error: Store ID is required for connection 'main' ``` **Solution**: Set `OPENFGA_STORE_ID` in your `.env` file ##### Invalid Authentication ``` OpenFGA Configuration Error: Invalid authentication method 'invalid' ``` **Solution**: Use one of: `none`, `api_token`, `client_credentials` ##### Missing Credentials ``` OpenFGA Configuration Error: API token required for 'api_token' authentication ``` **Solution**: Set `OPENFGA_API_TOKEN` in your `.env` file #### Configuration Caching For production, cache your configuration: ```bash php artisan config:cache ``` Clear cached configuration: ```bash php artisan config:clear ``` #### Debugging Configuration Use the debug command to inspect current configuration: ```bash php artisan openfga:debug ``` Output: ``` OpenFGA Configuration Debug =========================== Default Connection: main Connections: [main] URL: http://localhost:8080 Store ID: 01J5KGFHSDHGJSD123 Model ID: 01J5KGH7SDFGHJKL456 Auth Method: api_token Cache: enabled (300s TTL) Queue: disabled [production] URL: https://api.openfga.example.com Store ID: prod-store-123 Model ID: prod-model-456 Auth Method: client_credentials Cache: enabled (600s TTL) Queue: enabled Cache Status: ✓ Enabled (redis driver) Queue Status: ✗ Disabled Logging Status: ✓ Enabled (openfga channel) ``` #### Next Steps - Learn about [Eloquent Integration](https://github.com/evansims/openfga-laravel/blob/main/eloquent.md) - Configure [Middleware & Authorization](https://github.com/evansims/openfga-laravel/blob/main/middleware.md) - Set up [Testing](https://github.com/evansims/openfga-laravel/blob/main/testing.md) - Optimize with [Performance Guide](https://github.com/evansims/openfga-laravel/blob/main/performance.md) <!-- End of evansims/openfga-laravel/docs/configuration.md --> <!-- Source: evansims/openfga-laravel/docs/eloquent.md --> ### Eloquent Integration Guide This guide covers how to integrate OpenFGA authorization with your Eloquent models, providing a seamless authorization layer for your Laravel applications. #### Getting Started ##### Adding the Trait Add the `HasAuthorization` trait to any model that needs authorization: ```php <?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use OpenFGA\Laravel\Traits\HasAuthorization; class Document extends Model { use HasAuthorization; } ``` #### Basic Authorization Operations ##### Granting Permissions ```php $document = Document::find(1); $user = User::find(1); // Grant using model instance $document->grant($user, 'editor'); // Grant using user ID string $document->grant('user:123', 'viewer'); // Grant multiple permissions at once $document->grantMany([ ['user:alice', 'viewer'], ['user:bob', 'editor'], ['user:charlie', 'owner'], ]); ``` ##### Checking Permissions ```php // Check permission for a user model if ($document->check($user, 'editor')) { // User can edit the document } // Check permission for current authenticated user if ($document->check('@me', 'viewer')) { // Current user can view the document } // Check permission with user ID string if ($document->check('user:123', 'owner')) { // User 123 is the owner } ``` ##### Revoking Permissions ```php // Revoke single permission $document->revoke($user, 'editor'); // Revoke multiple permissions $document->revokeMany([ ['user:alice', 'viewer'], ['user:bob', 'editor'], ]); // Revoke all permissions for a user $document->revokeAll($user); // Revoke all permissions for everyone $document->revokeAllPermissions(); ``` #### Authorization Object Customization ##### Default Behavior By default, the authorization object is generated as `{model_type}:{id}`: ```php $document = Document::find(123); echo $document->authorizationObject(); // "document:123" ``` ##### Customizing the Type Override the `authorizationType()` method: ```php class Document extends Model { use HasAuthorization; protected function authorizationType(): string { return 'doc'; // Results in "doc:123" } } ``` ##### Customizing the Object Format Override the `authorizationObject()` method for complete control: ```php class Document extends Model { use HasAuthorization; public function authorizationObject(): string { return "document:{$this->team_id}:{$this->id}"; // Results in "document:team-5:123" } } ``` ##### Using UUIDs or Slugs ```php class Document extends Model { use HasAuthorization; public function authorizationObject(): string { // Use UUID if available if ($this->uuid) { return "document:{$this->uuid}"; } // Fall back to ID return "document:{$this->id}"; } } ``` #### Query Scopes ##### whereUserCan Scope Find all models that a user has specific permissions for: ```php // Get all documents the current user can view $viewableDocuments = Document::whereUserCan('@me', 'viewer')->get(); // Get all documents a specific user can edit $editableDocuments = Document::whereUserCan($user, 'editor')->get(); // Combine with other query constraints $recentEditableDocuments = Document::whereUserCan($user, 'editor') ->where('created_at', '>', now()->subDays(7)) ->orderBy('updated_at', 'desc') ->get(); ``` ##### whereUserCanAny Scope Find models where user has any of the specified permissions: ```php // Get documents where user can edit OR own $documents = Document::whereUserCanAny($user, ['editor', 'owner'])->get(); ``` ##### whereUserCanAll Scope Find models where user has all specified permissions: ```php // Get documents where user is both viewer AND editor $documents = Document::whereUserCanAll($user, ['viewer', 'editor'])->get(); ``` #### Model Events ##### Automatic Permission Cleanup The trait automatically cleans up permissions when a model is deleted: ```php $document->delete(); // All permissions for this document are automatically revoked ``` To disable automatic cleanup: ```php class Document extends Model { use HasAuthorization; protected $cleanupPermissionsOnDelete = false; } ``` Or globally in config: ```php // config/openfga.php 'options' => [ 'cleanup_on_delete' => false, ], ``` ##### Permission Events Listen for permission changes: ```php use OpenFGA\Laravel\Events\PermissionGranted; use OpenFGA\Laravel\Events\PermissionRevoked; // In EventServiceProvider protected $listen = [ PermissionGranted::class => [ SendPermissionNotification::class, UpdateAuditLog::class, ], PermissionRevoked::class => [ NotifyPermissionRevoked::class, CleanupRelatedData::class, ], ]; ``` #### Advanced Usage ##### Contextual Permissions Check permissions with contextual tuples: ```php $document->checkWithContext( $user, 'viewer', [ ['user:' . $user->id, 'member', 'team:engineering'], ] ); ``` ##### Batch Operations Perform multiple authorization operations efficiently: ```php // Queue batch operations $document->queueBatchWrite( writes: [ ['user:alice', 'viewer'], ['user:bob', 'editor'], ['user:charlie', 'owner'], ], deletes: [ ['user:david', 'viewer'], ] ); // Execute immediately $document->executeBatchWrite( writes: [ ['user:alice', 'viewer'], ['user:bob', 'editor'], ] ); ``` ##### Getting Users with Permissions ```php // Get all users who have a specific permission $editors = $document->getUsersWithPermission('editor'); // Get users with any of the specified permissions $users = $document->getUsersWithAnyPermission(['viewer', 'editor']); // Get permissions for a specific user $permissions = $document->getUserPermissions($user); // Returns: ['viewer', 'editor'] ``` ##### Permission Expansion Expand permissions to see the full authorization tree: ```php $expansion = $document->expandPermission('viewer'); // Get all users from expansion $users = $expansion->getUsers(); ``` #### Relationships and Permissions ##### Parent-Child Relationships ```php class Folder extends Model { use HasAuthorization; public function documents() { return $this->hasMany(Document::class); } // Grant permission to all documents when granting to folder public function grantWithChildren($user, $relation) { // Grant to folder $this->grant($user, $relation); // Grant to all documents $this->documents->each->grant($user, $relation); } } ``` ##### Many-to-Many Relationships ```php class Team extends Model { use HasAuthorization; public function users() { return $this->belongsToMany(User::class); } // Grant team permissions to all members public function grantToAllMembers($relation, $object) { $writes = $this->users->map(function ($user) use ($relation, $object) { return ["user:{$user->id}", $relation, $object]; })->toArray(); OpenFga::writeBatch(writes: $writes); } } ``` #### Performance Optimization ##### Eager Loading Permissions Create a custom attribute to cache permission checks: ```php class Document extends Model { use HasAuthorization; protected $appends = ['can_edit', 'can_view']; public function getCanEditAttribute() { return once(fn() => $this->check('@me', 'editor')); } public function getCanViewAttribute() { return once(fn() => $this->check('@me', 'viewer')); } } ``` ##### Batch Loading Permissions Load permissions for multiple models at once: ```php $documents = Document::limit(10)->get(); // Pre-load permissions $permissions = OpenFga::batchCheck( $documents->map(fn($doc) => [ 'user' => 'user:' . auth()->id(), 'relation' => 'viewer', 'object' => $doc->authorizationObject(), ])->toArray() ); // Use cached results foreach ($documents as $index => $document) { $canView = $permissions[$index]; } ``` ##### Using with Resources Integrate with API Resources: ```php class DocumentResource extends JsonResource { public function toArray($request) { return [ 'id' => $this->id, 'title' => $this->title, 'content' => $this->when( $this->check('@me', 'viewer'), $this->content ), 'permissions' => [ 'can_view' => $this->check('@me', 'viewer'), 'can_edit' => $this->check('@me', 'editor'), 'can_delete' => $this->check('@me', 'owner'), ], ]; } } ``` #### Testing with Models ##### Using Factories ```php use OpenFGA\Laravel\Testing\FakesOpenFga; class DocumentTest extends TestCase { use FakesOpenFga; public function test_user_can_edit_document() { $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); // Grant permission $document->grant($user, 'editor'); // Assert permission exists $this->assertTrue($document->check($user, 'editor')); // Assert OpenFGA was called OpenFga::assertGranted( "user:{$user->id}", 'editor', "document:{$document->id}" ); } } ``` ##### Testing Scopes ```php public function test_where_user_can_scope() { $this->fakeOpenFga(); $user = User::factory()->create(); $documents = Document::factory()->count(5)->create(); // Grant permissions to some documents $documents[0]->grant($user, 'viewer'); $documents[2]->grant($user, 'viewer'); $documents[4]->grant($user, 'viewer'); // Mock listObjects response OpenFga::shouldListObjects( "user:{$user->id}", 'viewer', 'document', ["document:{$documents[0]->id}", "document:{$documents[2]->id}", "document:{$documents[4]->id}"] ); // Test scope $viewableDocuments = Document::whereUserCan($user, 'viewer')->get(); $this->assertCount(3, $viewableDocuments); } ``` #### Common Patterns ##### Resource Ownership ```php class Document extends Model { use HasAuthorization; protected static function booted() { // Automatically grant owner permission to creator static::created(function (Document $document) { if (auth()->check()) { $document->grant('@me', 'owner'); } }); } public function isOwnedBy($user): bool { return $this->check($user, 'owner'); } } ``` ##### Hierarchical Permissions ```php class Document extends Model { use HasAuthorization; public function grantWithHierarchy($user, $relation) { $relations = ['viewer', 'editor', 'owner']; $index = array_search($relation, $relations); if ($index === false) { throw new InvalidArgumentException("Invalid relation: {$relation}"); } // Grant requested permission and all lower permissions for ($i = 0; $i <= $index; $i++) { $this->grant($user, $relations[$i]); } } } ``` ##### Temporary Permissions ```php class Document extends Model { use HasAuthorization; public function grantTemporary($user, $relation, $expiresAt) { $this->grant($user, $relation); // Schedule revocation dispatch(new RevokePermissionJob($this, $user, $relation)) ->delay($expiresAt); } } ``` #### Next Steps - Set up [Middleware & Authorization](https://github.com/evansims/openfga-laravel/blob/main/middleware.md) - Learn about [Testing](https://github.com/evansims/openfga-laravel/blob/main/testing.md) your models - Optimize with [Performance Guide](https://github.com/evansims/openfga-laravel/blob/main/performance.md) - Check the [API Reference](https://github.com/evansims/openfga-laravel/blob/main/api-reference.md) <!-- End of evansims/openfga-laravel/docs/eloquent.md --> <!-- Source: evansims/openfga-laravel/docs/installation.md --> ### Installation Guide This guide will walk you through installing and configuring the OpenFGA Laravel package in your Laravel application. #### Requirements - PHP 8.3 or higher - Laravel 12.0 or higher - OpenFGA server (self-hosted or cloud) - Composer #### Installation ##### Step 1: Install via Composer Install the package using Composer: ```bash composer require evansimsopenfga-laravel ``` ##### Step 2: Publish Configuration Publish the configuration file to customize your setup: ```bash php artisan vendor:publish --tag="openfga-config" ``` This will create a `config/openfga.php` file in your application. ##### Step 3: Configure Environment Variables Add the following environment variables to your `.env` file: ```env # Basic OpenFGA Configuration OPENFGA_URL=http://localhost:8080 OPENFGA_STORE_ID=your-store-id OPENFGA_MODEL_ID=your-model-id # Authentication (choose one method) OPENFGA_AUTH_METHOD=none # Options: none, api_token, client_credentials # For API Token authentication OPENFGA_API_TOKEN=your-api-token # For OAuth2 Client Credentials OPENFGA_CLIENT_ID=your-client-id OPENFGA_CLIENT_SECRET=your-client-secret OPENFGA_TOKEN_ISSUER=your-token-issuer OPENFGA_API_AUDIENCE=your-api-audience # Optional: Performance & Features OPENFGA_CACHE_ENABLED=true OPENFGA_CACHE_TTL=300 OPENFGA_QUEUE_ENABLED=false OPENFGA_LOGGING_ENABLED=true ``` ##### Step 4: Register Service Providers (Optional) The package's service providers are automatically discovered by Laravel. However, if you've disabled package discovery, add these to your `config/app.php`: ```php 'providers' => [ // ... OpenFGA\Laravel\OpenFgaServiceProvider::class, OpenFGA\Laravel\Providers\AuthorizationServiceProvider::class, OpenFGA\Laravel\Providers\BladeServiceProvider::class, OpenFGA\Laravel\Providers\EventServiceProvider::class, ], ``` ##### Step 5: Add Middleware (Optional) To use the authorization middleware, add it to your `app/Http/Kernel.php`: ```php protected $middlewareAliases = [ // ... 'openfga' => \OpenFGA\Laravel\Http\Middleware\OpenFgaMiddleware::class, 'can.any' => \OpenFGA\Laravel\Http\Middleware\RequiresAnyPermission::class, 'can.all' => \OpenFGA\Laravel\Http\Middleware\RequiresAllPermissions::class, ]; ``` ##### Step 6: Generate IDE Helper Files (Optional) For better IDE autocompletion and static analysis, generate the IDE helper files: ```bash # Generate IDE helper files php artisan ide-helper:generate php artisan ide-helper:models php artisan ide-helper:meta ``` The package automatically integrates with barryvdh/laravel-ide-helper when running in local environment, providing full autocompletion support for the OpenFGA facade and all its methods. #### Configuration Options ##### Multiple Connections You can configure multiple OpenFGA connections for different stores or environments: ```php // config/openfga.php return [ 'default' => env('OPENFGA_CONNECTION', 'main'), 'connections' => [ 'main' => [ 'url' => env('OPENFGA_URL'), 'store_id' => env('OPENFGA_STORE_ID'), 'model_id' => env('OPENFGA_MODEL_ID'), // ... ], 'secondary' => [ 'url' => env('OPENFGA_SECONDARY_URL'), 'store_id' => env('OPENFGA_SECONDARY_STORE_ID'), 'model_id' => env('OPENFGA_SECONDARY_MODEL_ID'), // ... ], ], ]; ``` ##### Caching Configure caching to improve performance: ```php 'cache' => [ 'enabled' => true, 'store' => 'redis', // Use your preferred cache store 'ttl' => 300, // Cache TTL in seconds 'prefix' => 'openfga', ], ``` ##### Queue Support Enable queue support for batch operations: ```php 'queue' => [ 'enabled' => true, 'connection' => 'redis', 'queue' => 'openfga', ], ``` #### Verifying Installation ##### Step 1: Check Configuration Run the debug command to verify your configuration: ```bash php artisan openfga:debug ``` ##### Step 2: Test Connection Test the connection to your OpenFGA server: ```bash php artisan openfga:check user:test reader document:test ``` ##### Step 3: Create Test Permission Grant a test permission to verify write operations: ```bash php artisan openfga:grant user:test writer document:test ``` #### Common Issues ##### Connection Refused If you're getting connection errors: 1. Verify your OpenFGA server is running 2. Check the `OPENFGA_URL` is correct 3. Ensure no firewall is blocking the connection ##### Invalid Store ID If you're getting store-related errors: 1. Verify the `OPENFGA_STORE_ID` exists 2. Check you have proper permissions for the store 3. Try creating a new store if needed ##### Authentication Errors For authentication issues: 1. Verify your credentials are correct 2. Check the authentication method matches your server configuration 3. Ensure tokens/credentials haven't expired #### Next Steps - Read the [Quick Start Tutorial](https://github.com/evansims/openfga-laravel/blob/main/quickstart.md) to learn basic usage - Explore [Configuration Options](https://github.com/evansims/openfga-laravel/blob/main/configuration.md) for advanced settings - Check out [Eloquent Integration](https://github.com/evansims/openfga-laravel/blob/main/eloquent.md) for model authorization - Learn about [Testing](https://github.com/evansims/openfga-laravel/blob/main/testing.md) with the package #### Support If you encounter any issues: 1. Check the [Troubleshooting Guide](https://github.com/evansims/openfga-laravel/blob/main/troubleshooting.md) 2. Search existing [GitHub Issues](https://github.com/evansims/openfga-laravel/issues) 3. Create a new issue with detailed information about your problem #### Version Compatibility | Laravel Version | Package Version | PHP Version | | --------------- | --------------- | ----------- | | 10.x | 1.x | 8.1+ | | 11.x | 1.x | 8.2+ | <!-- End of evansims/openfga-laravel/docs/installation.md --> <!-- Source: evansims/openfga-laravel/docs/middleware.md --> ### Middleware & Authorization Guide This guide covers how to protect your routes and integrate OpenFGA with Laravel's authorization system using middleware, gates, and policies. #### Middleware Setup ##### Registering Middleware Register the middleware in your `app/Http/Kernel.php`: ```php protected $middlewareAliases = [ // ... other middleware 'openfga' => \OpenFGA\Laravel\Http\Middleware\OpenFgaMiddleware::class, 'can.any' => \OpenFGA\Laravel\Http\Middleware\RequiresAnyPermission::class, 'can.all' => \OpenFGA\Laravel\Http\Middleware\RequiresAllPermissions::class, ]; ``` #### Basic Route Protection ##### Single Permission Check Protect routes requiring a specific permission: ```php // User must have 'editor' permission on document Route::middleware(['auth', 'openfga:editor,document:{document}']) ->get('/documents/{document}/edit', [DocumentController::class, 'edit']); // User must be owner of the team Route::middleware(['auth', 'openfga:owner,team:{team}']) ->get('/teams/{team}/settings', [TeamController::class, 'settings']); ``` ##### Dynamic Object Resolution The middleware automatically resolves route parameters: ```php // Middleware will check: user:123#viewer@document:456 Route::middleware(['auth', 'openfga:viewer,document:{document}']) ->get('/documents/{document}', [DocumentController::class, 'show']); // For nested resources Route::middleware(['auth', 'openfga:member,team:{team}']) ->get('/teams/{team}/documents/{document}', function (Team $team, Document $document) { // Both $team and $document are available }); ``` #### Multiple Permission Checks ##### Any Permission (OR Logic) User needs at least one of the specified permissions: ```php // User can view OR edit OR own the document Route::middleware(['auth', 'can.any:viewer,editor,owner,document:{document}']) ->get('/documents/{document}', [DocumentController::class, 'show']); // More complex example Route::middleware([ 'auth', 'can.any:admin,moderator,owner,forum:{forum}' ])->group(function () { Route::get('/forums/{forum}/manage', [ForumController::class, 'manage']); Route::post('/forums/{forum}/settings', [ForumController::class, 'updateSettings']); }); ``` ##### All Permissions (AND Logic) User needs all specified permissions: ```php // User must be both a member AND have billing permission Route::middleware(['auth', 'can.all:member,billing_manager,team:{team}']) ->get('/teams/{team}/billing', [BillingController::class, 'index']); // Multiple requirements Route::middleware([ 'auth', 'can.all:verified_user,premium_member,active,user:{user}' ])->get('/premium-content', [PremiumController::class, 'index']); ``` #### Advanced Middleware Usage ##### Custom Object Resolution Create custom middleware for complex object resolution: ```php namespace App\Http\Middleware; use OpenFGA\Laravel\Http\Middleware\OpenFgaMiddleware; class ProjectPermission extends OpenFgaMiddleware { protected function resolveObject($request): string { $project = $request->route('project'); $team = $project->team; // Check permission on team instead of project return "team:{$team->id}"; } } ``` ##### Contextual Permissions Add contextual tuples in middleware: ```php namespace App\Http\Middleware; use OpenFGA\Laravel\Http\Middleware\OpenFgaMiddleware; class ContextualPermission extends OpenFgaMiddleware { protected function getContextualTuples($request): array { $user = $request->user(); $organization = $user->organization; return [ ["user:{$user->id}", 'member', "organization:{$organization->id}"], ["user:{$user->id}", 'employee', "company:{$organization->company_id}"], ]; } } ``` ##### Connection-Specific Checks Use different OpenFGA connections: ```php Route::middleware(['auth', 'openfga:admin,system:core,connection:admin']) ->get('/admin', [AdminController::class, 'dashboard']); ``` #### Gate Integration ##### Registering Gates The package automatically registers gates based on your permissions: ```php // In AuthServiceProvider or OpenFgaServiceProvider use Illuminate\Support\Facades\Gate; use OpenFGA\Laravel\Facades\OpenFga; public function boot() { // Define a gate using OpenFGA Gate::define('edit-document', function ($user, $document) { return OpenFga::check( "user:{$user->id}", 'editor', "document:{$document->id}" ); }); // Dynamic gate registration $relations = ['viewer', 'editor', 'owner']; foreach ($relations as $relation) { Gate::define("{$relation}-document", function ($user, $document) use ($relation) { return OpenFga::check( "user:{$user->id}", $relation, "document:{$document->id}" ); }); } } ``` ##### Using Gates in Controllers ```php class DocumentController extends Controller { public function edit(Document $document) { // Using Gate facade if (Gate::denies('edit-document', $document)) { abort(403); } // Using authorize helper $this->authorize('editor-document', $document); // Using can method if (!auth()->user()->can('edit-document', $document)) { abort(403); } return view('documents.edit', compact('document')); } } ``` ##### Gate Responses Return detailed responses from gates: ```php Gate::define('manage-team', function ($user, $team) { if (!OpenFga::check("user:{$user->id}", 'admin', "team:{$team->id}")) { return Response::deny('You must be a team admin to manage settings.'); } if (!$team->is_active) { return Response::deny('This team is inactive.'); } return Response::allow(); }); ``` #### Policy Classes ##### Creating an OpenFGA Policy ```php namespace App\Policies; use App\Models\User; use App\Models\Document; use OpenFGA\Laravel\Policies\OpenFgaPolicy; class DocumentPolicy extends OpenFgaPolicy { public function view(User $user, Document $document): bool { return $this->check($user, 'viewer', $document); } public function update(User $user, Document $document): bool { return $this->check($user, 'editor', $document); } public function delete(User $user, Document $document): bool { return $this->check($user, 'owner', $document); } public function restore(User $user, Document $document): bool { return $this->check($user, 'owner', $document); } public function forceDelete(User $user, Document $document): bool { return $this->check($user, 'owner', $document) && $this->check($user, 'admin', 'system:documents'); } } ``` ##### Advanced Policy Methods ```php class DocumentPolicy extends OpenFgaPolicy { public function viewAny(User $user): bool { // Check if user can view any documents return $this->check($user, 'member', 'system:documents'); } public function create(User $user): bool { // Check if user can create documents return $this->check($user, 'creator', 'system:documents') && $user->hasActiveSubscription(); } public function share(User $user, Document $document): bool { // Custom method for sharing return $this->check($user, 'owner', $document) || $this->check($user, 'editor', $document); } public function publish(User $user, Document $document): bool { // Multiple checks with context return $this->checkWithContext( $user, 'publisher', $document, [ ["user:{$user->id}", 'verified', 'system:users'], ] ); } } ``` ##### Registering Policies Register your policies in `AuthServiceProvider`: ```php protected $policies = [ Document::class => DocumentPolicy::class, Team::class => TeamPolicy::class, Project::class => ProjectPolicy::class, ]; ``` #### Form Request Authorization ##### Basic Form Request ```php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use OpenFGA\Laravel\Traits\AuthorizesWithOpenFga; class UpdateDocumentRequest extends FormRequest { use AuthorizesWithOpenFga; public function authorize(): bool { $document = $this->route('document'); return $this->checkPermission('editor', $document); } public function rules(): array { return [ 'title' => 'required|string|max:255', 'content' => 'required|string', 'tags' => 'array', 'tags.*' => 'string|max:50', ]; } } ``` ##### Complex Authorization Logic ```php class PublishDocumentRequest extends FormRequest { use AuthorizesWithOpenFga; public function authorize(): bool { $document = $this->route('document'); $user = $this->user(); // Must be editor or owner if (!$this->checkAnyPermission(['editor', 'owner'], $document)) { return false; } // Must have publishing rights if (!$this->checkPermission('publisher', 'system:documents')) { return false; } // Document must be in draft state return $document->status === 'draft'; } protected function failedAuthorization() { throw new AuthorizationException('You are not authorized to publish this document.'); } } ``` ##### Contextual Form Requests ```php class TransferOwnershipRequest extends FormRequest { use AuthorizesWithOpenFga; public function authorize(): bool { $document = $this->route('document'); $team = $document->team; // Check with contextual tuples return $this->checkPermissionWithContext( 'owner', $document, [ ["user:{$this->user()->id}", 'admin', "team:{$team->id}"], ] ); } } ``` #### Middleware Groups ##### Creating Permission-Based Route Groups ```php // routes/web.php Route::middleware(['auth'])->group(function () { // Public routes - only need authentication Route::get('/dashboard', [DashboardController::class, 'index']); // Editor routes Route::middleware(['can.any:editor,admin,document:{document}'])->group(function () { Route::get('/documents/{document}/edit', [DocumentController::class, 'edit']); Route::put('/documents/{document}', [DocumentController::class, 'update']); }); // Admin routes Route::middleware(['openfga:admin,system:core'])->prefix('admin')->group(function () { Route::get('/', [AdminController::class, 'index']); Route::get('/users', [AdminController::class, 'users']); Route::get('/settings', [AdminController::class, 'settings']); }); }); ``` ##### Resource Controllers with Middleware ```php Route::resource('documents', DocumentController::class) ->middleware([ 'index' => 'openfga:viewer,system:documents', 'create' => 'openfga:creator,system:documents', 'store' => 'openfga:creator,system:documents', 'show' => 'openfga:viewer,document:{document}', 'edit' => 'openfga:editor,document:{document}', 'update' => 'openfga:editor,document:{document}', 'destroy' => 'openfga:owner,document:{document}', ]); ``` #### API Authentication ##### Protecting API Routes ```php // routes/api.php Route::middleware(['auth:sanctum'])->group(function () { Route::middleware(['openfga:api_user,system:api'])->group(function () { Route::apiResource('documents', Api\DocumentController::class); }); Route::middleware(['openfga:api_admin,system:api'])->prefix('admin')->group(function () { Route::get('/stats', [Api\AdminController::class, 'stats']); Route::get('/audit', [Api\AdminController::class, 'audit']); }); }); ``` ##### Token-Based Permissions ```php namespace App\Http\Middleware; use OpenFGA\Laravel\Http\Middleware\OpenFgaMiddleware; class ApiPermission extends OpenFgaMiddleware { protected function resolveUser($request): string { $token = $request->user()->currentAccessToken(); // Use token ID for API permissions return "token:{$token->id}"; } } ``` #### Error Handling ##### Custom Error Responses ```php namespace App\Http\Middleware; use OpenFGA\Laravel\Http\Middleware\OpenFgaMiddleware; class CustomPermissionMiddleware extends OpenFgaMiddleware { protected function handleUnauthorized($request, $relation, $object) { if ($request->expectsJson()) { return response()->json([ 'error' => 'Insufficient permissions', 'required' => $relation, 'object' => $object, ], 403); } return redirect() ->route('access-denied') ->with('error', "You need {$relation} permission to access this resource."); } } ``` ##### Logging Failed Attempts ```php class AuditedPermissionMiddleware extends OpenFgaMiddleware { protected function handleUnauthorized($request, $relation, $object) { Log::warning('Unauthorized access attempt', [ 'user' => $request->user()->id, 'relation' => $relation, 'object' => $object, 'ip' => $request->ip(), 'url' => $request->fullUrl(), ]); return parent::handleUnauthorized($request, $relation, $object); } } ``` #### Testing Middleware ##### Testing Route Protection ```php use OpenFGA\Laravel\Testing\FakesOpenFga; class DocumentMiddlewareTest extends TestCase { use FakesOpenFga; public function test_editor_can_access_edit_page() { $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); OpenFga::grant("user:{$user->id}", 'editor', "document:{$document->id}"); $response = $this->actingAs($user) ->get("/documents/{$document->id}/edit"); $response->assertOk(); } public function test_viewer_cannot_access_edit_page() { $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); OpenFga::grant("user:{$user->id}", 'viewer', "document:{$document->id}"); $response = $this->actingAs($user) ->get("/documents/{$document->id}/edit"); $response->assertForbidden(); } } ``` ##### Testing Multiple Permissions ```php public function test_any_permission_middleware() { $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); // User only has viewer permission OpenFga::grant("user:{$user->id}", 'viewer', "document:{$document->id}"); // Route requires viewer OR editor OR owner $response = $this->actingAs($user) ->get("/documents/{$document->id}/details"); $response->assertOk(); // Verify the correct permission was checked OpenFga::assertChecked("user:{$user->id}", 'viewer', "document:{$document->id}"); } ``` #### Performance Tips ##### Preload Permissions ```php namespace App\Http\Middleware; class PreloadPermissions { public function handle($request, Closure $next) { if ($user = $request->user()) { // Preload common permissions $permissions = OpenFga::batchCheck([ ["user:{$user->id}", 'member', 'system:app'], ["user:{$user->id}", 'premium', 'system:features'], ["user:{$user->id}", 'admin', 'system:core'], ]); // Store in request for later use $request->attributes->set('user_permissions', $permissions); } return $next($request); } } ``` ##### Cache Middleware Results ```php class CachedPermissionMiddleware extends OpenFgaMiddleware { protected function checkPermission($user, $relation, $object): bool { $cacheKey = "permission:{$user}:{$relation}:{$object}"; return Cache::remember($cacheKey, 300, function () use ($user, $relation, $object) { return parent::checkPermission($user, $relation, $object); }); } } ``` #### Next Steps - Learn about [Testing](https://github.com/evansims/openfga-laravel/blob/main/testing.md) authorization - Optimize with [Performance Guide](https://github.com/evansims/openfga-laravel/blob/main/performance.md) - See [Troubleshooting Guide](https://github.com/evansims/openfga-laravel/blob/main/troubleshooting.md) for common issues - Check the [API Reference](https://github.com/evansims/openfga-laravel/blob/main/api-reference.md) <!-- End of evansims/openfga-laravel/docs/middleware.md --> <!-- Source: evansims/openfga-laravel/docs/migration-guide.md --> ### Migration Guide: From Traditional Authorization to OpenFGA Laravel This guide helps you migrate from existing Laravel authorization systems to OpenFGA Laravel, providing step-by-step instructions, code examples, and best practices. #### Table of Contents - [Overview](#overview) - [From Laravel's Built-in Authorization](#from-laravels-built-in-authorization) - [From Spatie Laravel Permission](#from-spatie-laravel-permission) - [From Laravel Sanctum/Passport with Roles](#from-laravel-sanctumpassport-with-roles) - [From Custom Authorization Systems](#from-custom-authorization-systems) - [Migration Strategies](#migration-strategies) - [Common Challenges and Solutions](#common-challenges-and-solutions) - [Testing Your Migration](#testing-your-migration) #### Overview OpenFGA provides fine-grained, relationship-based authorization that scales better than traditional role-based systems. This guide covers migrating from various Laravel authorization patterns. ##### Why Migrate to OpenFGA? - **Fine-grained permissions**: Move beyond simple roles to relationship-based authorization - **Scalability**: Handle complex permission hierarchies without performance degradation - **Flexibility**: Model real-world authorization scenarios accurately - **Auditability**: Track permission changes with detailed logging - **Performance**: Efficient permission checking with built-in caching #### From Laravel's Built-in Authorization Laravel's built-in authorization using Gates and Policies can be enhanced with OpenFGA's relationship-based model. ##### Before: Laravel Gates ```php // app/Providers/AuthServiceProvider.php Gate::define('edit-post', function (User $user, Post $post) { return $user->id === $post->user_id || $user->isAdmin(); }); Gate::define('view-admin-panel', function (User $user) { return $user->role === 'admin'; }); ``` ##### After: OpenFGA Laravel ```php // 1. Define your authorization model // openfga/model.fga model schema 1.1 type user type organization relations define admin: [user] define member: [user] or admin type post relations define owner: [user] define editor: [user] or owner define viewer: [user] or editor or member from organization // 2. Use in your application class PostController extends Controller { public function edit(Post $post) { $this->authorize('edit', $post); // Uses OpenFGA policy return view('posts.edit', compact('post')); } } // 3. Create OpenFGA Policy class PostPolicy { use \OpenFGA\Laravel\Traits\AuthorizesWithOpenFga; public function edit(User $user, Post $post): bool { return $this->check($user->authorizationUser(), 'editor', $post->authorizationObject()); } } ``` ##### Migration Steps 1. **Install OpenFGA Laravel**: ```bash composer require evansims/openfga-laravel php artisan vendor:publish --provider="OpenFGA\Laravel\OpenFgaServiceProvider" ``` 2. **Define your authorization model** based on existing Gates: ```bash php artisan openfga:model:create ``` 3. **Replace Gate definitions** with OpenFGA policies: ```php // Instead of Gate::define(), create Policy classes php artisan make:policy PostPolicy --model=Post ``` 4. **Migrate existing permissions**: ```php // Create a migration command php artisan make:command MigrateToOpenFga class MigrateToOpenFga extends Command { public function handle() { User::chunk(100, function ($users) { foreach ($users as $user) { if ($user->role === 'admin') { // Grant admin access to organization OpenFga::grant($user->authorizationUser(), 'admin', 'organization:main'); } // Migrate post ownership foreach ($user->posts as $post) { OpenFga::grant($user->authorizationUser(), 'owner', $post->authorizationObject()); } } }); } } ``` #### From Spatie Laravel Permission Spatie's Laravel Permission package uses roles and permissions. OpenFGA provides more flexibility with relationships. ##### Before: Spatie Laravel Permission ```php // Roles and permissions $user->assignRole('editor'); $user->givePermissionTo('edit articles'); // Checking permissions if ($user->can('edit articles')) { // Allow editing } // Role-based checks if ($user->hasRole('admin')) { // Admin actions } ``` ##### After: OpenFGA Laravel ```php // Define relationships in authorization model type user type role relations define assignee: [user] type organization relations define admin: [user] define editor: [user] or admin define member: [user] or editor type article relations define owner: [user] define editor: [user] or owner or editor from organization define viewer: [user] or editor // Grant relationships OpenFga::grant('user:123', 'editor', 'organization:acme'); OpenFga::grant('user:123', 'owner', 'article:456'); // Check permissions if (OpenFga::check('user:123', 'editor', 'article:456')) { // Allow editing } ``` ##### Compatibility Layer Create a compatibility layer to ease migration: ```php // app/Services/SpatieMigrationService.php class SpatieMigrationService { public function migrateUser(User $user): void { // Migrate roles foreach ($user->roles as $role) { $this->migrateRole($user, $role); } // Migrate direct permissions foreach ($user->permissions as $permission) { $this->migratePermission($user, $permission); } } private function migrateRole(User $user, Role $role): void { match ($role->name) { 'admin' => OpenFga::grant($user->authorizationUser(), 'admin', 'organization:main'), 'editor' => OpenFga::grant($user->authorizationUser(), 'editor', 'organization:main'), 'member' => OpenFga::grant($user->authorizationUser(), 'member', 'organization:main'), default => $this->handleCustomRole($user, $role), }; } private function migratePermission(User $user, Permission $permission): void { // Map Spatie permissions to OpenFGA relationships $mapping = [ 'edit articles' => ['editor', 'article:*'], 'view admin panel' => ['admin', 'organization:main'], 'manage users' => ['admin', 'organization:main'], ]; if (isset($mapping[$permission->name])) { [$relation, $object] = $mapping[$permission->name]; OpenFga::grant($user->authorizationUser(), $relation, $object); } } } ``` ##### Migration Command ```php class MigrateFromSpatie extends Command { protected $signature = 'openfga:migrate:spatie'; protected $description = 'Migrate from Spatie Laravel Permission to OpenFGA'; public function handle(SpatieMigrationService $migrationService) { $this->info('Starting migration from Spatie Laravel Permission...'); $userCount = User::count(); $bar = $this->output->createProgressBar($userCount); User::with(['roles', 'permissions'])->chunk(100, function ($users) use ($migrationService, $bar) { foreach ($users as $user) { $migrationService->migrateUser($user); $bar->advance(); } }); $bar->finish(); $this->newLine(); $this->info('Migration completed successfully!'); } } ``` #### From Laravel Sanctum/Passport with Roles API authentication with roles can be enhanced with OpenFGA's fine-grained permissions. ##### Before: Sanctum with Abilities ```php // Create token with abilities $token = $user->createToken('API Token', ['read-posts', 'write-posts']); // Check abilities in middleware Route::middleware(['auth:sanctum', 'abilities:write-posts'])->post('/posts', [PostController::class, 'store']); // In controller if ($request->user()->tokenCan('write-posts')) { // Allow action } ``` ##### After: OpenFGA Laravel with API ```php // API middleware using OpenFGA class OpenFgaApiMiddleware { public function handle($request, Closure $next, $relation, $objectType = null) { $user = $request->user(); $object = $objectType ?? $this->resolveObjectFromRoute($request); if (!OpenFga::check($user->authorizationUser(), $relation, $object)) { return response()->json(['error' => 'Insufficient permissions'], 403); } return $next($request); } } // Routes with OpenFGA protection Route::middleware(['auth:sanctum', 'openfga:editor,post'])->post('/posts', [PostController::class, 'store']); Route::middleware(['auth:sanctum', 'openfga:viewer,post'])->get('/posts/{post}', [PostController::class, 'show']); ``` ##### API Permission Checking Endpoint ```php // routes/api.php Route::middleware('auth:sanctum')->post('/permissions/check', [PermissionController::class, 'check']); // app/Http/Controllers/Api/PermissionController.php class PermissionController extends Controller { public function check(Request $request) { $request->validate([ 'checks' => 'required|array', 'checks.*.relation' => 'required|string', 'checks.*.object' => 'required|string', ]); $user = $request->user(); $results = []; foreach ($request->checks as $check) { $results[] = OpenFga::check( $user->authorizationUser(), $check['relation'], $check['object'] ); } return response()->json(['results' => $results]); } } ``` #### From Custom Authorization Systems If you have a custom authorization system, migration requires mapping your existing logic to OpenFGA relationships. ##### Analysis Phase 1. **Audit existing permissions**: ```php php artisan make:command AnalyzePermissions class AnalyzePermissions extends Command { public function handle() { // Analyze existing permission patterns $this->analyzeUserRoles(); $this->analyzeResourcePermissions(); $this->analyzePermissionInheritance(); } private function analyzeUserRoles() { $roles = DB::table('user_roles')->distinct()->pluck('role'); $this->table(['Existing Roles'], $roles->map(fn($role) => [$role])); } private function analyzeResourcePermissions() { // Analyze how permissions are currently structured $permissions = DB::table('permissions') ->select('resource_type', 'action', DB::raw('count(*) as count')) ->groupBy('resource_type', 'action') ->get(); $this->table(['Resource Type', 'Action', 'Count'], $permissions->toArray()); } } ``` 2. **Map to OpenFGA model**: ```fga model schema 1.1 type user type department relations define manager: [user] define member: [user] or manager type project relations define owner: [user] define collaborator: [user] or member from department define viewer: [user] or collaborator type document relations define author: [user] define editor: [user] or author or collaborator from project define viewer: [user] or editor ``` ##### Migration Strategy ```php class CustomSystemMigration { public function migrate() { $this->migrateDepartments(); $this->migrateProjects(); $this->migrateDocuments(); $this->migrateUserRelationships(); } private function migrateDepartments() { Department::chunk(50, function ($departments) { foreach ($departments as $dept) { // Grant manager access if ($dept->manager) { OpenFga::grant( $dept->manager->authorizationUser(), 'manager', $dept->authorizationObject() ); } // Grant member access foreach ($dept->members as $member) { OpenFga::grant( $member->authorizationUser(), 'member', $dept->authorizationObject() ); } } }); } } ``` #### Migration Strategies ##### 1. Gradual Migration (Recommended) Migrate functionality piece by piece while maintaining existing system: ```php // Feature flag approach class AuthorizationService { public function check(User $user, string $permission, $resource = null): bool { if (config('features.openfga_authorization')) { return $this->checkWithOpenFga($user, $permission, $resource); } return $this->checkWithLegacySystem($user, $permission, $resource); } private function checkWithOpenFga(User $user, string $permission, $resource): bool { $relation = $this->mapPermissionToRelation($permission); $object = $resource ? $resource->authorizationObject() : 'organization:main'; return OpenFga::check($user->authorizationUser(), $relation, $object); } } ``` ##### 2. Shadow Mode Run both systems in parallel for comparison: ```php class ShadowModeAuthorization { public function check(User $user, string $permission, $resource = null): bool { $legacyResult = $this->legacyCheck($user, $permission, $resource); if (config('features.openfga_shadow_mode')) { $openFgaResult = $this->openFgaCheck($user, $permission, $resource); // Log discrepancies for analysis if ($legacyResult !== $openFgaResult) { Log::warning('Authorization mismatch', [ 'user_id' => $user->id, 'permission' => $permission, 'resource' => $resource?->getKey(), 'legacy_result' => $legacyResult, 'openfga_result' => $openFgaResult, ]); } } return $legacyResult; } } ``` ##### 3. Big Bang Migration Complete migration all at once (for smaller applications): ```php class BigBangMigration extends Command { public function handle() { DB::transaction(function () { $this->info('Starting complete migration...'); // Disable legacy authorization config(['features.legacy_auth' => false]); // Migrate all data $this->migrateAllUsers(); $this->migrateAllResources(); $this->migrateAllPermissions(); // Enable OpenFGA config(['features.openfga_auth' => true]); $this->info('Migration completed!'); }); } } ``` #### Common Challenges and Solutions ##### 1. Complex Permission Hierarchies **Challenge**: Existing complex role hierarchies don't map directly to OpenFGA relationships. **Solution**: Use OpenFGA's relationship inheritance: ```fga type organization relations define owner: [user] define admin: [user] or owner define manager: [user] or admin define member: [user] or manager ``` ##### 2. Dynamic Permissions **Challenge**: Permissions that change based on context or time. **Solution**: Use contextual tuples and condition evaluation: ```php // Grant temporary access OpenFga::grant('user:123', 'editor', 'document:456', [ 'expires_at' => now()->addDays(7)->toISOString() ]); // Check with context $allowed = OpenFga::check('user:123', 'editor', 'document:456', [ 'current_time' => now()->toISOString() ]); ``` ##### 3. Performance During Migration **Challenge**: Migration queries can be slow for large datasets. **Solution**: Use batching and caching: ```php class PerformantMigration { public function migrateInBatches() { $batchSize = 1000; $offset = 0; do { $tuples = $this->prepareTuples($offset, $batchSize); if (!empty($tuples)) { OpenFga::writeBatch($tuples); $this->info("Migrated batch starting at offset {$offset}"); } $offset += $batchSize; } while (count($tuples) === $batchSize); } private function prepareTuples(int $offset, int $limit): array { return User::offset($offset) ->limit($limit) ->with(['roles', 'permissions']) ->get() ->flatMap(fn($user) => $this->userToTuples($user)) ->toArray(); } } ``` ##### 4. Data Consistency **Challenge**: Ensuring data remains consistent during migration. **Solution**: Use database transactions and verification: ```php class ConsistentMigration { public function migrate() { DB::transaction(function () { $this->performMigration(); $this->verifyMigration(); }); } private function verifyMigration() { $sampleUsers = User::inRandomOrder()->limit(100)->get(); foreach ($sampleUsers as $user) { $legacyPermissions = $this->getLegacyPermissions($user); $openFgaPermissions = $this->getOpenFgaPermissions($user); if (!$this->permissionsMatch($legacyPermissions, $openFgaPermissions)) { throw new Exception("Migration verification failed for user {$user->id}"); } } } } ``` #### Testing Your Migration ##### 1. Unit Tests ```php class MigrationTest extends TestCase { use RefreshDatabase, FakesOpenFga; public function test_admin_permissions_migrated_correctly() { $admin = User::factory()->create(); $admin->assignRole('admin'); // Legacy system $migration = new SpatieMigrationService(); $migration->migrateUser($admin); $this->assertPermissionGranted( $admin->authorizationUser(), 'admin', 'organization:main' ); } public function test_resource_ownership_migrated() { $user = User::factory()->create(); $post = Post::factory()->create(['user_id' => $user->id]); $migration = new LegacyMigrationService(); $migration->migratePost($post); $this->assertPermissionGranted( $user->authorizationUser(), 'owner', $post->authorizationObject() ); } } ``` ##### 2. Integration Tests ```php class AuthorizationIntegrationTest extends TestCase { public function test_migrated_permissions_work_in_controllers() { $editor = User::factory()->create(); OpenFga::grant($editor->authorizationUser(), 'editor', 'organization:main'); $response = $this->actingAs($editor) ->post('/posts', ['title' => 'Test Post']); $response->assertSuccessful(); } } ``` ##### 3. Performance Tests ```php class MigrationPerformanceTest extends TestCase { public function test_migration_completes_within_time_limit() { $startTime = microtime(true); $migration = new FullMigration(); $migration->migrate(); $duration = microtime(true) - $startTime; $this->assertLessThan(300, $duration, 'Migration should complete within 5 minutes'); } } ``` #### Post-Migration Checklist - [ ] All user permissions migrated correctly - [ ] All resource relationships established - [ ] Legacy authorization system disabled - [ ] Performance benchmarks met - [ ] All tests passing - [ ] Documentation updated - [ ] Team trained on new system - [ ] Monitoring and alerts configured - [ ] Rollback plan tested - [ ] Security audit completed #### Rollback Strategy Always have a rollback plan: ```php class RollbackMigration extends Command { public function handle() { if (!$this->confirm('Are you sure you want to rollback to the legacy system?')) { return; } // Re-enable legacy system config(['features.legacy_auth' => true]); config(['features.openfga_auth' => false]); // Clear OpenFGA data if needed if ($this->confirm('Clear OpenFGA authorization data?')) { $this->clearOpenFgaData(); } $this->info('Rollback completed successfully'); } } ``` #### Support and Resources - **Documentation**: Refer to the [OpenFGA Laravel documentation](https://github.com/evansims/openfga-laravel/blob/main/docs/README.md) - **Community**: Join discussions in GitHub issues - **Examples**: Check the [example application](https://github.com/evansims/openfga-laravel/blob/main/example/README.md) - **Support**: For migration assistance, create a GitHub issue with the `migration` label #### Next Steps After completing your migration: 1. **Optimize Performance**: Implement caching strategies for your use case 2. **Enhance Security**: Set up proper audit logging and monitoring 3. **Expand Usage**: Explore advanced OpenFGA features like conditions and contextual tuples 4. **Team Training**: Ensure your team understands the new authorization model This migration guide provides a comprehensive approach to moving from traditional Laravel authorization to OpenFGA. Take time to plan your migration strategy and test thoroughly in a staging environment before deploying to production. <!-- End of evansims/openfga-laravel/docs/migration-guide.md --> <!-- Source: evansims/openfga-laravel/docs/multi-tenancy.md --> ### Multi-Tenancy Support OpenFGA Laravel provides first-class support for multi-tenancy, allowing you to manage authorization for multiple tenants using different stores, models, or even different OpenFGA servers. #### Configuration Define multiple connections in your `config/openfga.php`: ```php return [ 'default' => env('OPENFGA_CONNECTION', 'main'), 'connections' => [ 'main' => [ 'url' => env('OPENFGA_URL', 'http://localhost:8080'), 'store_id' => env('OPENFGA_STORE_ID'), 'model_id' => env('OPENFGA_MODEL_ID'), 'credentials' => [ 'method' => env('OPENFGA_AUTH_METHOD', 'none'), ], ], 'tenant_a' => [ 'url' => env('TENANT_A_OPENFGA_URL'), 'store_id' => env('TENANT_A_STORE_ID'), 'model_id' => env('TENANT_A_MODEL_ID'), 'credentials' => [ 'method' => 'api_token', 'token' => env('TENANT_A_API_TOKEN'), ], ], 'tenant_b' => [ 'url' => env('TENANT_B_OPENFGA_URL'), 'store_id' => env('TENANT_B_STORE_ID'), 'model_id' => env('TENANT_B_MODEL_ID'), 'credentials' => [ 'method' => 'client_credentials', 'client_id' => env('TENANT_B_CLIENT_ID'), 'client_secret' => env('TENANT_B_CLIENT_SECRET'), 'api_token_issuer' => env('TENANT_B_TOKEN_ISSUER'), 'api_audience' => env('TENANT_B_API_AUDIENCE'), ], ], ], ]; ``` #### Usage Patterns ##### Per-Request Connection Selection Use a specific connection for individual operations: ```php use OpenFGA\Laravel\Facades\OpenFga; // Check permission for tenant A $allowed = OpenFga::connection('tenant_a')->check( user: 'user:123', relation: 'viewer', object: 'document:456' ); // List objects for tenant B $documents = OpenFga::connection('tenant_b')->listObjects( user: 'user:123', relation: 'viewer', type: 'document' ); ``` ##### Switching Default Connection Change the default connection for all subsequent operations: ```php // Set default connection for current request OpenFga::setConnection('tenant_a'); // All subsequent operations use tenant_a connection OpenFga::check('user:123', 'viewer', 'document:456'); OpenFga::grant('user:123', 'editor', 'document:789'); // Switch to another tenant OpenFga::setConnection('tenant_b'); ``` ##### Middleware-Based Tenant Resolution Automatically set the connection based on the current tenant: ```php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use OpenFGA\Laravel\Facades\OpenFga; class SetOpenFgaTenant { public function handle(Request $request, Closure $next) { // Resolve tenant from request (e.g., subdomain, header, session) $tenant = $request->route('tenant') ?? $request->header('X-Tenant-ID') ?? session('tenant_id'); if ($tenant) { // Map tenant to connection name $connection = $this->mapTenantToConnection($tenant); OpenFga::setConnection($connection); } return $next($request); } private function mapTenantToConnection(string $tenant): string { return match ($tenant) { 'acme' => 'tenant_a', 'globex' => 'tenant_b', default => 'main', }; } } ``` Register the middleware: ```php // In app/Http/Kernel.php protected $middlewareGroups = [ 'api' => [ // ... other middleware \App\Http\Middleware\SetOpenFgaTenant::class, ], ]; ``` ##### Model-Level Tenant Isolation Use the `HasAuthorization` trait with tenant-specific connections: ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use OpenFGA\Laravel\Traits\HasAuthorization; class Document extends Model { use HasAuthorization; /** * Get the OpenFGA connection for this model. */ protected function authorizationConnection(): ?string { // Return connection based on model's tenant return $this->tenant?->openfga_connection ?? 'main'; } } // Usage $document = Document::find(1); $document->grant($user, 'viewer'); // Uses tenant-specific connection ``` ##### Query Builder with Tenant Context ```php use OpenFGA\Laravel\Facades\OpenFga; // Create tenant-specific query builders $tenantAQuery = OpenFga::query('tenant_a'); $tenantBQuery = OpenFga::query('tenant_b'); // Perform operations on specific tenants $allowedInA = $tenantAQuery ->for('user:123') ->can('view') ->on('document:456') ->check(); $documentsInB = $tenantBQuery ->for('user:123') ->can('edit') ->type('document') ->listObjects(); ``` #### Cache Isolation Each connection maintains its own cache namespace: ```php // Cache keys are automatically namespaced by store ID 'cache' => [ 'enabled' => true, 'prefix' => 'openfga', // Base prefix // Actual cache key: openfga:{store_id}:check:user:123:viewer:document:456 ], ``` #### Best Practices ##### 1. Connection Naming Use descriptive connection names that reflect their purpose: ```php 'connections' => [ 'production_us_east' => [...], 'production_eu_west' => [...], 'staging' => [...], 'customer_acme' => [...], 'customer_globex' => [...], ], ``` ##### 2. Environment Configuration Use environment-specific configuration files: ```bash # .env.production OPENFGA_CONNECTION=production_main PRODUCTION_MAIN_URL=https://api.openfga.prod.example.com PRODUCTION_MAIN_STORE_ID=01HQMVAH3R8XPROD123456 # .env.staging OPENFGA_CONNECTION=staging STAGING_URL=https://api.openfga.staging.example.com STAGING_STORE_ID=01HQMVAH3R8XSTAGE123456 ``` ##### 3. Connection Pooling Enable connection pooling for better performance: ```php 'connections' => [ 'high_traffic_tenant' => [ // ... other config 'pool' => [ 'enabled' => true, 'max_connections' => 20, 'min_connections' => 5, ], ], ], ``` ##### 4. Health Monitoring Monitor all tenant connections: ```php use OpenFGA\Laravel\Facades\OpenFga; // Check health of all connections $health = OpenFga::healthCheckAll(); foreach ($health as $connection => $status) { if (!$status['healthy']) { Log::error("OpenFGA connection {$connection} is unhealthy", $status); } } ``` ##### 5. Testing with Multiple Connections ```php use OpenFGA\Laravel\Testing\OpenFgaTestCase; class MultiTenantTest extends OpenFgaTestCase { public function test_tenant_isolation(): void { // Mock different responses for different connections $this->mockOpenFga('tenant_a') ->shouldCheckPermission( user: 'user:123', relation: 'viewer', object: 'document:456' ) ->andReturn(true); $this->mockOpenFga('tenant_b') ->shouldCheckPermission( user: 'user:123', relation: 'viewer', object: 'document:456' ) ->andReturn(false); // Test isolation $this->assertTrue( OpenFga::connection('tenant_a')->check('user:123', 'viewer', 'document:456') ); $this->assertFalse( OpenFga::connection('tenant_b')->check('user:123', 'viewer', 'document:456') ); } } ``` #### Migration Between Tenants Transfer permissions between tenants: ```php use OpenFGA\Laravel\Facades\OpenFga; class TenantMigration { public function migrateTenant(string $from, string $to, string $userId): void { // Export permissions from source tenant $permissions = OpenFga::connection($from) ->export() ->forUser($userId) ->get(); // Import to target tenant OpenFga::connection($to) ->import($permissions) ->execute(); } } ``` #### Troubleshooting ##### Connection Not Found ```php // This will throw an exception OpenFga::connection('nonexistent')->check(...); // Safe connection checking if (config("openfga.connections.{$tenant}")) { $result = OpenFga::connection($tenant)->check(...); } else { // Handle missing connection Log::warning("Unknown tenant connection: {$tenant}"); } ``` ##### Performance Considerations 1. **Connection Overhead**: Each connection maintains its own HTTP client. Reuse connections when possible. 2. **Cache Warming**: Pre-warm caches for frequently accessed tenants: ```php Artisan::call('openfga:cache:warm', ['--connection' => 'tenant_a']); ``` 3. **Batch Operations**: Group operations by tenant to minimize connection switching: ```php // Inefficient foreach ($items as $item) { OpenFga::connection($item->tenant)->check(...); } // Efficient $itemsByTenant = $items->groupBy('tenant'); foreach ($itemsByTenant as $tenant => $tenantItems) { OpenFga::setConnection($tenant); OpenFga::batchCheck($tenantItems->map(...)); } ``` #### See Also - [Configuration](https://github.com/evansims/openfga-laravel/blob/main/configuration.md) - Detailed configuration options - [Performance](https://github.com/evansims/openfga-laravel/blob/main/performance.md) - Performance optimization strategies - [Testing](https://github.com/evansims/openfga-laravel/blob/main/testing.md) - Testing multi-tenant applications <!-- End of evansims/openfga-laravel/docs/multi-tenancy.md --> <!-- Source: evansims/openfga-laravel/docs/performance.md --> ### OpenFGA Laravel - Performance Best Practices This guide covers performance optimization strategies for OpenFGA Laravel to ensure your authorization system scales efficiently. #### Table of Contents - [Caching Strategies](#caching-strategies) - [Batch Operations](#batch-operations) - [Connection Pooling](#connection-pooling) - [Queue Integration](#queue-integration) - [Database Optimization](#database-optimization) - [Monitoring & Profiling](#monitoring--profiling) - [Common Pitfalls](#common-pitfalls) #### Caching Strategies ##### Enable Caching The most significant performance improvement comes from enabling caching: ```env OPENFGA_CACHE_ENABLED=true OPENFGA_CACHE_TTL=300 # 5 minutes ``` ##### Use Tagged Caching For granular cache invalidation, use a cache store that supports tagging (Redis, Memcached): ```env OPENFGA_CACHE_STORE=redis OPENFGA_CACHE_TAGS_ENABLED=true ``` ##### Cache Warming Pre-load frequently checked permissions: ```php use OpenFGA\Laravel\Facades\OpenFga; // Warm cache for common permissions $commonChecks = [ ['user:123', 'read', 'document:456'], ['user:123', 'write', 'document:456'], // ... ]; OpenFga::batchCheck($commonChecks); ``` Use the artisan command for bulk warming: ```bash php artisan openfga:warm-cache --user=123 --relations=read,write --type=document ``` ##### Write-Behind Cache For non-critical permission updates, enable write-behind caching: ```env OPENFGA_WRITE_BEHIND_ENABLED=true OPENFGA_WRITE_BEHIND_BATCH_SIZE=100 OPENFGA_WRITE_BEHIND_FLUSH_INTERVAL=5 ``` #### Batch Operations ##### Batch Checks Always use batch checks when checking multiple permissions: ```php // ❌ Inefficient - Multiple API calls $canRead = OpenFga::check('user:123', 'read', 'document:1'); $canWrite = OpenFga::check('user:123', 'write', 'document:1'); $canDelete = OpenFga::check('user:123', 'delete', 'document:1'); // ✅ Efficient - Single API call $results = OpenFga::batchCheck([ ['user:123', 'read', 'document:1'], ['user:123', 'write', 'document:1'], ['user:123', 'delete', 'document:1'], ]); ``` ##### Batch Writes Group permission changes together: ```php // ❌ Inefficient - Multiple API calls OpenFga::grant('user:123', 'read', 'document:1'); OpenFga::grant('user:123', 'write', 'document:1'); OpenFga::grant('user:456', 'read', 'document:1'); // ✅ Efficient - Single API call OpenFga::writeBatch([ ['user:123', 'read', 'document:1'], ['user:123', 'write', 'document:1'], ['user:456', 'read', 'document:1'], ]); ``` ##### Eloquent Batch Operations Use collection methods for bulk model operations: ```php // Grant permissions to multiple users efficiently $document->grantBulk($users, 'read'); // Check permissions for multiple models $readableDocuments = Document::whereUserCan($user, 'read') ->chunk(1000, function ($documents) { // Process documents in chunks }); ``` #### Connection Pooling Enable connection pooling for high-traffic applications: ```env OPENFGA_POOL_ENABLED=true OPENFGA_POOL_MAX_CONNECTIONS=10 OPENFGA_POOL_MIN_CONNECTIONS=2 ``` Configure pool settings based on your load: ```php // config/openfga.php 'pool' => [ 'enabled' => true, 'max_connections' => env('OPENFGA_POOL_MAX_CONNECTIONS', 10), 'min_connections' => env('OPENFGA_POOL_MIN_CONNECTIONS', 2), 'max_idle_time' => env('OPENFGA_POOL_MAX_IDLE_TIME', 300), 'connection_timeout' => env('OPENFGA_POOL_CONNECTION_TIMEOUT', 5), ], ``` #### Queue Integration ##### Async Permission Updates Use queues for non-critical permission updates: ```env OPENFGA_QUEUE_ENABLED=true OPENFGA_QUEUE_CONNECTION=redis OPENFGA_QUEUE_NAME=openfga-high ``` Example usage: ```php use OpenFGA\Laravel\Jobs\BatchWriteJob; // Queue permission updates BatchWriteJob::dispatch($writes, $deletes) ->onQueue('openfga-low'); // Use different queue priorities ``` ##### Queue Worker Configuration Optimize queue workers for OpenFGA jobs: ```bash # High priority queue for critical updates php artisan queue:work redis --queue=openfga-high --tries=3 --timeout=30 # Low priority queue for bulk operations php artisan queue:work redis --queue=openfga-low --tries=5 --timeout=120 ``` #### Database Optimization ##### Deduplication Enable request deduplication to prevent duplicate API calls: ```php use OpenFGA\Laravel\Deduplication\RequestDeduplicator; // Deduplication is automatic for identical concurrent requests $results = OpenFga::withDeduplication()->batchCheck($checks); ``` ##### Optimize Contextual Tuples When using contextual tuples, batch them efficiently: ```php // Include all necessary context in a single check $allowed = OpenFga::check('user:123', 'view', 'document:456', [ 'contextual_tuples' => [ ['user:123', 'member', 'team:789'], ['team:789', 'has_access', 'folder:abc'], ], ]); ``` #### Monitoring & Profiling ##### Enable Profiling Enable profiling to identify bottlenecks: ```env OPENFGA_PROFILING_ENABLED=true OPENFGA_SLOW_QUERY_THRESHOLD=100 ``` ##### View Profiling Data ```bash # View current profile php artisan openfga:profile # Show only slow queries php artisan openfga:profile --slow # Export as JSON for analysis php artisan openfga:profile --json > profile.json ``` ##### Laravel Debugbar Integration The package automatically integrates with Laravel Debugbar when available: ```env OPENFGA_DEBUGBAR_ENABLED=true ``` ##### Custom Metrics Track custom metrics in your application: ```php use OpenFGA\Laravel\Profiling\OpenFgaProfiler; $profiler = app(OpenFgaProfiler::class); $profile = $profiler->startProfile('custom_operation', ['key' => 'value']); // Your operation here $profile->end(true); $profile->addMetadata('result_count', 42); ``` #### Common Pitfalls ##### 1. N+1 Query Problem Avoid checking permissions in loops: ```php // ❌ Bad - N+1 queries foreach ($documents as $document) { if (OpenFga::check($user, 'read', $document->authorizationObject())) { // Process document } } // ✅ Good - Batch check $checks = $documents->map(fn($doc) => [ $user, 'read', $doc->authorizationObject() ])->toArray(); $results = OpenFga::batchCheck($checks); ``` ##### 2. Unnecessary Cache Invalidation Be strategic about cache invalidation: ```php // ❌ Bad - Invalidates entire cache Cache::tags(['openfga'])->flush(); // ✅ Good - Invalidate specific entries OpenFga::forgetCached('user:123', 'read', 'document:456'); ``` ##### 3. Over-Fetching Relations Use `listObjects` efficiently: ```php // ❌ Bad - Fetches all objects then filters $allDocuments = OpenFga::listObjects('user:123', 'read', 'document'); $recentDocuments = collect($allDocuments)->filter(...); // ✅ Good - Filter at database level $documentIds = OpenFga::listObjects('user:123', 'read', 'document'); $recentDocuments = Document::whereIn('id', $documentIds) ->where('created_at', '>', now()->subDays(7)) ->get(); ``` ##### 4. Synchronous Write-Behind Flushes Avoid synchronous flushes in request cycle: ```php // ❌ Bad - Blocks request app(WriteBehindCache::class)->flush(); // ✅ Good - Use scheduled job // In app/Console/Kernel.php $schedule->command('openfga:flush-write-behind')->everyMinute(); ``` #### Performance Benchmarks Run benchmarks to measure your setup: ```bash # Basic benchmark php artisan openfga:benchmark # Detailed benchmark with custom parameters php artisan openfga:benchmark --operations=10000 --concurrent=100 --duration=60 ``` Expected performance targets: - **Cached checks**: < 1ms - **Single API check**: < 50ms - **Batch check (100 items)**: < 200ms - **Write operations**: < 100ms - **List operations**: < 200ms #### Optimization Checklist - [ ] Enable caching with appropriate TTL - [ ] Use tagged caching if available - [ ] Implement cache warming for hot paths - [ ] Use batch operations wherever possible - [ ] Enable connection pooling for high traffic - [ ] Configure queue workers for async operations - [ ] Enable request deduplication - [ ] Set up monitoring and profiling - [ ] Regular benchmark testing - [ ] Review slow query logs weekly #### Advanced Optimizations ##### Custom Cache Keys Implement custom cache key strategies: ```php use OpenFGA\Laravel\Cache\CacheKeyGenerator; class CustomCacheKeyGenerator extends CacheKeyGenerator { public function generate(string $user, string $relation, string $object): string { // Include tenant context in cache key $tenant = tenant()->id; return "openfga:{$tenant}:{$user}:{$relation}:{$object}"; } } ``` ##### Precomputed Permissions For extremely high-performance requirements, consider precomputing permissions: ```php // Scheduled job to precompute common permissions class PrecomputePermissions extends Command { public function handle() { $users = User::active()->get(); $documents = Document::recent()->get(); $checks = []; foreach ($users as $user) { foreach ($documents as $document) { foreach (['read', 'write'] as $relation) { $checks[] = [ "user:{$user->id}", $relation, $document->authorizationObject(), ]; } } } // Warm cache with batch check OpenFga::batchCheck($checks); } } ``` #### Conclusion Following these best practices will ensure your OpenFGA Laravel implementation scales efficiently. Remember to: 1. **Measure first** - Use profiling to identify actual bottlenecks 2. **Cache aggressively** - Most permission checks are read-heavy 3. **Batch operations** - Reduce API calls through batching 4. **Monitor continuously** - Set up alerts for slow queries 5. **Optimize iteratively** - Start with quick wins, then tackle complex optimizations For more help, see our [Troubleshooting Guide](https://github.com/evansims/openfga-laravel/blob/main/TROUBLESHOOTING.md) or visit the [OpenFGA documentation](https://openfga.dev/docs). <!-- End of evansims/openfga-laravel/docs/performance.md --> <!-- Source: evansims/openfga-laravel/docs/quickstart.md --> ### Quickstart Tutorial This tutorial will get you up and running with OpenFGA Laravel in just a few minutes. We'll build a simple document management system where users can have different permissions on documents. #### Prerequisites Make sure you've completed the [installation guide](https://github.com/evansims/openfga-laravel/blob/main/installation.md) and have: - OpenFGA Laravel package installed - OpenFGA server running - Basic configuration in place #### Setting Up Your Authorization Model First, let's define a simple authorization model. Create a file called `document-model.openfga`: ```dsl model schema 1.1 type user type document relations define owner: [user] define editor: [user] or owner define viewer: [user] or editor ``` Load this model into your OpenFGA store using the OpenFGA CLI or API. #### How It Works Understanding how OpenFGA Laravel processes authorization requests helps you build more efficient applications. Here's the complete request lifecycle: ```mermaid flowchart TD Start([Application Request]) --> Gate{Laravel Gate?} Gate -->|Yes| GateProvider[OpenFgaGateProvider] Gate -->|No| DirectCall[Direct OpenFga Call] GateProvider --> Manager[OpenFgaManager] DirectCall --> Manager Manager --> Cache{Cache Enabled?} Cache -->|Yes| CacheCheck{Cache Hit?} Cache -->|No| ClientCall[OpenFGA Client] CacheCheck -->|Hit| CacheReturn[Return Cached Result] CacheCheck -->|Miss| ClientCall ClientCall --> Connection[Connection Pool] Connection --> APICall[OpenFGA API Request] APICall --> Response[API Response] Response --> StoreCache{Should Cache?} StoreCache -->|Yes| CacheStore[Store in Cache] StoreCache -->|No| ReturnResult[Return Result] CacheStore --> ReturnResult CacheReturn --> ReturnResult ReturnResult --> WriteBehind{Write-Behind Cache?} WriteBehind -->|Yes| QueueJob[Queue Write Job] WriteBehind -->|No| End([Result]) QueueJob --> End style Manager fill:#e1f5fe style Cache fill:#f3e5f5 style ClientCall fill:#e8f5e8 style APICall fill:#fff3e0 ``` **Key Components:** 1. **Laravel Gate Integration**: Seamlessly integrates with Laravel's authorization system 2. **OpenFgaManager**: Central coordination point with connection pooling and multi-tenancy 3. **Caching Layer**: Optional Redis/database caching for improved performance 4. **Connection Pool**: Manages multiple OpenFGA connections for different tenants 5. **Write-Behind Cache**: Queued write operations for better response times 6. **OpenFGA API**: The actual authorization service #### Basic Usage ##### 1. Granting Permissions Let's grant a user permission to view a document: ```php use OpenFGA\Laravel\Facades\OpenFga; // Grant viewer permission OpenFga::grant('user:alice', 'viewer', 'document:budget-2024'); // Grant editor permission OpenFga::grant('user:bob', 'editor', 'document:budget-2024'); // Grant owner permission OpenFga::grant('user:charlie', 'owner', 'document:budget-2024'); ``` ##### 2. Checking Permissions Check if a user has permission: ```php // Check single permission if (OpenFga::check('user:alice', 'viewer', 'document:budget-2024')) { echo "Alice can view the document"; } // Check using the current authenticated user if (OpenFga::check('@me', 'editor', 'document:budget-2024')) { echo "Current user can edit the document"; } ``` ##### 3. Using the Query Builder For more complex operations, use the query builder: ```php // Check with contextual tuples $canEdit = OpenFga::query() ->for('user:alice') ->can('editor') ->on('document:budget-2024') ->with(['user:alice', 'member', 'team:finance']) ->check(); // Batch operations OpenFga::query() ->grant([ ['user:alice', 'viewer', 'document:report-1'], ['user:alice', 'viewer', 'document:report-2'], ['user:alice', 'viewer', 'document:report-3'], ]) ->execute(); ``` #### Eloquent Integration ##### 1. Add the Trait to Your Model ```php namespace App\Models; use Illuminate\Database\Eloquent\Model; use OpenFGA\Laravel\Traits\HasAuthorization; class Document extends Model { use HasAuthorization; // Optional: customize the authorization type protected function authorizationType(): string { return 'document'; } } ``` ##### 2. Working with Model Permissions ```php $document = Document::find(1); // Grant permission $document->grant($user, 'editor'); $document->grant('user:alice', 'viewer'); // Check permission if ($document->check($user, 'editor')) { // User can edit } // Revoke permission $document->revoke($user, 'editor'); // Get all users with a specific permission $editors = $document->getUsersWithPermission('editor'); ``` ##### 3. Query by Permissions ```php // Get all documents the user can view $documents = Document::whereUserCan($user, 'viewer')->get(); // Get documents where current user is owner $myDocuments = Document::whereUserCan('@me', 'owner')->get(); ``` #### Route Protection ##### 1. Using Middleware ```php // In routes/web.php Route::middleware(['auth', 'openfga:editor,document:{document}']) ->get('/documents/{document}/edit', [DocumentController::class, 'edit']); // Multiple permissions (any) Route::middleware(['auth', 'can.any:viewer,editor,document:{document}']) ->get('/documents/{document}', [DocumentController::class, 'show']); // Multiple permissions (all required) Route::middleware(['auth', 'can.all:viewer,member,document:{document}']) ->get('/documents/{document}/share', [DocumentController::class, 'share']); ``` ##### 2. Controller Authorization ```php namespace App\Http\Controllers; use App\Models\Document; use OpenFGA\Laravel\Facades\OpenFga; class DocumentController extends Controller { public function show(Document $document) { // Manual check if (!OpenFga::check('@me', 'viewer', $document->authorizationObject())) { abort(403); } return view('documents.show', compact('document')); } public function update(Request $request, Document $document) { // Using Laravel's authorize method $this->authorize('editor', $document); $document->update($request->validated()); return redirect()->route('documents.show', $document); } } ``` #### Blade Templates ##### 1. Conditional Rendering ```blade @can('editor', 'document:' . $document->id) <a href="{{ route('documents.edit', $document) }}">Edit Document</a> @endcan @cannot('owner', 'document:' . $document->id) <p>You don't own this document</p> @endcannot @canany(['editor', 'owner'], 'document:' . $document->id) <button>Delete Document</button> @endcanany ``` ##### 2. Using Components ```blade <x-openfga-can relation="viewer" :object="$document->authorizationObject()"> <div class="document-content"> {{ $document->content }} </div> </x-openfga-can> <x-openfga-can-any :relations="['editor', 'owner']" :object="$document->authorizationObject()"> <div class="document-actions"> <button>Edit</button> <button>Share</button> </div> </x-openfga-can-any> ``` #### Form Requests Create authorized form requests: ```php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use OpenFGA\Laravel\Traits\AuthorizesWithOpenFga; class UpdateDocumentRequest extends FormRequest { use AuthorizesWithOpenFga; public function authorize(): bool { return $this->checkPermission('editor', $this->route('document')); } public function rules(): array { return [ 'title' => 'required|string|max:255', 'content' => 'required|string', ]; } } ``` #### Testing ##### 1. Using Fake Implementation ```php use OpenFGA\Laravel\Testing\FakesOpenFga; use Tests\TestCase; class DocumentTest extends TestCase { use FakesOpenFga; public function test_user_can_view_document() { $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); // Grant permission in fake OpenFga::grant("user:{$user->id}", 'viewer', "document:{$document->id}"); // Test the permission $response = $this->actingAs($user) ->get("/documents/{$document->id}"); $response->assertOk(); // Assert permission was checked OpenFga::assertChecked("user:{$user->id}", 'viewer', "document:{$document->id}"); } } ``` ##### 2. Testing Specific Scenarios ```php public function test_editor_can_update_document() { $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); // Setup permissions OpenFga::grant("user:{$user->id}", 'editor', "document:{$document->id}"); $response = $this->actingAs($user) ->put("/documents/{$document->id}", [ 'title' => 'Updated Title', 'content' => 'Updated content', ]); $response->assertRedirect(); $this->assertDatabaseHas('documents', [ 'id' => $document->id, 'title' => 'Updated Title', ]); } ``` #### Best Practices ##### 1. Use Type Prefixes Always prefix your identifiers: - ✅ `user:123` - ✅ `document:abc-123` - ❌ `123` - ❌ `abc-123` ##### 2. Cache Permissions Enable caching for better performance: ```php // config/openfga.php 'cache' => [ 'enabled' => true, 'ttl' => 300, // 5 minutes ], ``` ##### 3. Use Batch Operations When granting/revoking multiple permissions: ```php // Good - Single batch operation OpenFga::writeBatch( writes: [ ['user:alice', 'viewer', 'document:1'], ['user:alice', 'viewer', 'document:2'], ['user:alice', 'viewer', 'document:3'], ] ); // Avoid - Multiple individual calls OpenFga::grant('user:alice', 'viewer', 'document:1'); OpenFga::grant('user:alice', 'viewer', 'document:2'); OpenFga::grant('user:alice', 'viewer', 'document:3'); ``` ##### 4. Clean Up Permissions When deleting models, clean up their permissions: ```php class Document extends Model { use HasAuthorization; protected static function booted() { static::deleting(function (Document $document) { $document->revokeAllPermissions(); }); } } ``` #### Next Steps - Learn about [Configuration Options](https://github.com/evansims/openfga-laravel/blob/main/configuration.md) - Explore [Eloquent Integration](https://github.com/evansims/openfga-laravel/blob/main/eloquent.md) in depth - Understand [Middleware & Authorization](https://github.com/evansims/openfga-laravel/blob/main/middleware.md) - Set up [Testing](https://github.com/evansims/openfga-laravel/blob/main/testing.md) for your application - Optimize with our [Performance Guide](https://github.com/evansims/openfga-laravel/blob/main/performance.md) #### Getting Help - Check the [API Reference](https://github.com/evansims/openfga-laravel/blob/main/api-reference.md) - See the [Troubleshooting Guide](https://github.com/evansims/openfga-laravel/blob/main/troubleshooting.md) - Visit our [GitHub repository](https://github.com/evansims/openfga-laravel) <!-- End of evansims/openfga-laravel/docs/quickstart.md --> <!-- Source: evansims/openfga-laravel/docs/spatie-compatibility.md --> ### Spatie Laravel Permission Compatibility OpenFGA Laravel provides a comprehensive compatibility layer for [Spatie Laravel Permission](https://spatie.be/docs/laravel-permission), allowing you to migrate from role-based permissions to relationship-based authorization with minimal code changes. #### Table of Contents - [Overview](#overview) - [Installation](#installation) - [Configuration](#configuration) - [User Model Integration](#user-model-integration) - [Blade Directives](#blade-directives) - [Middleware](#middleware) - [Migration from Spatie](#migration-from-spatie) - [API Reference](#api-reference) - [Limitations](#limitations) #### Overview The Spatie compatibility layer provides: - **Familiar API**: Use the same method names as Spatie Laravel Permission - **Automatic Mapping**: Roles and permissions are automatically mapped to OpenFGA relations - **Blade Directives**: All Spatie Blade directives work with OpenFGA - **Middleware**: Drop-in replacement for Spatie middleware - **Migration Tools**: Commands to migrate existing Spatie data to OpenFGA #### Installation ##### 1. Enable Compatibility Add to your `.env` file: ```env OPENFGA_SPATIE_COMPATIBILITY=true ``` ##### 2. Publish Configuration ```bash php artisan vendor:publish --tag=openfga-spatie-config ``` ##### 3. Register Service Provider (Optional) If not using Laravel's auto-discovery, add to `config/app.php`: ```php 'providers' => [ // ... OpenFGA\Laravel\Providers\SpatieCompatibilityServiceProvider::class, ], ``` #### Configuration The compatibility layer is configured in `config/spatie-compatibility.php`: ```php return [ // Enable compatibility features 'enabled' => env('OPENFGA_SPATIE_COMPATIBILITY', false), // Map Spatie permissions to OpenFGA relations 'permission_mappings' => [ 'edit posts' => 'editor', 'view posts' => 'viewer', 'delete posts' => 'owner', 'manage users' => 'admin', // ... more mappings ], // Map Spatie roles to OpenFGA relations 'role_mappings' => [ 'admin' => 'admin', 'editor' => 'editor', 'moderator' => 'moderator', 'user' => 'member', // ... more mappings ], // Default context for role/permission checks 'default_context' => 'organization:main', // Enable/disable specific Blade directives 'blade_directives' => [ 'hasrole' => true, 'haspermission' => true, // ... more directives ], ]; ``` ##### Custom Mappings You can add custom mappings programmatically: ```php use OpenFGA\Laravel\Compatibility\SpatieCompatibility; $compatibility = app(SpatieCompatibility::class); // Add permission mapping $compatibility->addPermissionMapping('custom permission', 'custom_relation'); // Add role mapping $compatibility->addRoleMapping('custom role', 'custom_relation'); ``` #### User Model Integration ##### Add the Compatibility Trait ```php <?php namespace App\Models; use Illuminate\Foundation\Auth\User as Authenticatable; use OpenFGA\Laravel\Traits\{HasAuthorization, SpatieCompatible}; class User extends Authenticatable { use HasAuthorization, SpatieCompatible; // ... rest of your model } ``` ##### Using Spatie Methods Once the trait is added, you can use all familiar Spatie methods: ```php $user = User::find(1); // Role methods $user->assignRole('admin'); $user->removeRole('editor'); $user->hasRole('admin'); // true $user->hasAnyRole(['admin', 'editor']); // true $user->hasAllRoles(['admin', 'editor']); // false if user only has admin // Permission methods $user->givePermissionTo('edit posts'); $user->revokePermissionTo('delete posts'); $user->hasPermissionTo('edit posts'); // true $user->hasAnyPermission(['edit posts', 'view posts']); // true // Get collections $user->getRoleNames(); // Collection of role names $user->getAllPermissions(); // Collection of permissions // Sync methods $user->syncRoles(['admin', 'editor']); $user->syncPermissions(['edit posts', 'view posts']); ``` ##### Contextual Permissions OpenFGA supports contextual permissions, which you can use with the compatibility layer: ```php // Check role in specific organization $user->hasRole('admin', 'organization:acme'); // Assign role in specific context $user->assignRole('manager', 'department:engineering'); // Check permission on specific model $post = Post::find(1); $user->hasPermissionTo('edit posts', $post); ``` #### Blade Directives All Spatie Blade directives are supported: ##### Role Directives ```blade @hasrole('admin') <p>You are an admin!</p> @endhasrole @hasanyrole('admin|editor') <p>You are an admin or editor!</p> @endhasanyrole @hasallroles('admin|editor') <p>You are both admin and editor!</p> @endhasallroles @unlessrole('admin') <p>You are not an admin.</p> @endunlessrole ``` ##### Permission Directives ```blade @haspermission('edit posts') <a href="/posts/create">Create Post</a> @endhaspermission @hasanypermission('edit posts|view posts') <p>You can work with posts!</p> @endhasanypermission @hasallpermissions('edit posts|delete posts') <p>You have full post control!</p> @endhasallpermissions @unlesspermission('edit posts') <p>You cannot edit posts.</p> @endunlesspermission ``` ##### With Guard Support ```blade @role('admin', 'api') <p>Admin via API guard</p> @endrole @permission('edit posts', 'web') <p>Can edit posts via web guard</p> @endpermission ``` #### Middleware The compatibility layer provides drop-in replacements for Spatie middleware: ##### Register Middleware The middleware is automatically registered when compatibility is enabled. You can use them in routes: ```php // Role middleware Route::middleware(['auth', 'role:admin'])->group(function () { Route::get('/admin', [AdminController::class, 'index']); }); // Permission middleware Route::middleware(['auth', 'permission:edit posts'])->group(function () { Route::resource('posts', PostController::class); }); // Multiple roles (OR logic) Route::middleware(['auth', 'role:admin|editor'])->group(function () { Route::get('/content', [ContentController::class, 'index']); }); // Multiple permissions (OR logic) Route::middleware(['auth', 'permission:edit posts|edit articles'])->group(function () { Route::get('/content/edit', [ContentController::class, 'edit']); }); ``` ##### Controller Usage ```php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; class PostController extends Controller { public function __construct() { $this->middleware('permission:view posts')->only(['index', 'show']); $this->middleware('permission:edit posts')->only(['create', 'store', 'edit', 'update']); $this->middleware('permission:delete posts')->only(['destroy']); } // ... controller methods } ``` #### Migration from Spatie ##### Automatic Migration Use the migration command to automatically transfer your Spatie data to OpenFGA: ```bash # Dry run to see what would be migrated php artisan openfga:migrate:spatie --dry-run # Perform the actual migration php artisan openfga:migrate:spatie # Migrate with verification php artisan openfga:migrate:spatie --verify # Migrate in smaller batches php artisan openfga:migrate:spatie --batch-size=50 ``` ##### Continuous Synchronization For applications that need to keep Spatie permissions synchronized with OpenFGA, use the sync command: ```bash # Sync permissions from Spatie to OpenFGA php artisan openfga:sync-spatie-permissions # Dry run to see what would be synced php artisan openfga:sync-spatie-permissions --dry-run # Full sync (removes OpenFGA permissions not in Spatie) php artisan openfga:sync-spatie-permissions --full # Sync with custom batch size php artisan openfga:sync-spatie-permissions --batch-size=200 # Quiet mode (less output) php artisan openfga:sync-spatie-permissions --quiet-mode ``` This command is useful when: - You're gradually migrating from Spatie to OpenFGA - You need to maintain both systems temporarily - You want to schedule periodic syncs (e.g., via cron) - You're testing the migration in a staging environment Example cron job for periodic sync: ```cron # Sync permissions every hour 0 * * * * cd /path/to/your/app && php artisan openfga:sync-spatie-permissions --quiet-mode >> /dev/null 2>&1 ``` ##### Manual Migration If you prefer manual control, you can migrate specific parts: ```php use OpenFGA\Laravel\Compatibility\SpatieCompatibility; $compatibility = app(SpatieCompatibility::class); // Migrate a user's roles $user = User::find(1); foreach ($user->roles as $role) { $compatibility->assignRole($user, $role->name); } // Migrate a user's direct permissions foreach ($user->permissions as $permission) { $compatibility->givePermissionTo($user, $permission->name); } ``` ##### Post-Migration Steps 1. **Test thoroughly**: Verify all permissions work as expected 2. **Update authorization model**: Consider optimizing your OpenFGA model 3. **Remove Spatie**: Once confident, remove the Spatie package 4. **Clean up**: Remove old Spatie tables if desired ```bash # Remove Spatie package (when ready) composer remove spatie/laravel-permission # Drop Spatie tables (optional, after backup) php artisan migrate:rollback --path=vendor/spatie/laravel-permission/database/migrations ``` #### API Reference ##### SpatieCompatibility Class ```php use OpenFGA\Laravel\Compatibility\SpatieCompatibility; $compatibility = app(SpatieCompatibility::class); // Permission methods $compatibility->hasPermissionTo($user, 'edit posts', $model); $compatibility->hasAnyPermission($user, ['edit posts', 'view posts'], $model); $compatibility->hasAllPermissions($user, ['edit posts', 'delete posts'], $model); $compatibility->givePermissionTo($user, 'edit posts', $model); $compatibility->revokePermissionTo($user, 'edit posts', $model); // Role methods $compatibility->hasRole($user, 'admin', $context); $compatibility->hasAnyRole($user, ['admin', 'editor'], $context); $compatibility->hasAllRoles($user, ['admin', 'editor'], $context); $compatibility->assignRole($user, 'admin', $context); $compatibility->removeRole($user, 'admin', $context); // Collection methods $compatibility->getAllPermissions($user, $context); $compatibility->getRoleNames($user, $context); // Sync methods $compatibility->syncRoles($user, ['admin', 'editor'], $context); $compatibility->syncPermissions($user, ['edit posts', 'view posts'], $model); // Mapping methods $compatibility->addPermissionMapping($permission, $relation); $compatibility->addRoleMapping($role, $relation); $compatibility->getPermissionMappings(); $compatibility->getRoleMappings(); ``` ##### SpatieCompatible Trait Methods All methods from the `SpatieCompatibility` class are available on models using the trait: ```php $user = User::find(1); // Permission methods $user->hasPermissionTo('edit posts'); $user->hasAnyPermission(['edit posts', 'view posts']); $user->hasAllPermissions(['edit posts', 'delete posts']); $user->givePermissionTo('edit posts'); $user->revokePermissionTo('edit posts'); $user->can('edit posts'); // Alias for hasPermissionTo $user->cannot('edit posts'); // Opposite of hasPermissionTo // Role methods $user->hasRole('admin'); $user->hasAnyRole(['admin', 'editor']); $user->hasAllRoles(['admin', 'editor']); $user->assignRole('admin'); $user->removeRole('admin'); // Collection methods $user->getAllPermissions(); $user->getRoleNames(); $user->roles(); // Returns object collection for compatibility $user->permissions(); // Returns object collection for compatibility // Sync methods $user->syncRoles(['admin', 'editor']); $user->syncPermissions(['edit posts', 'view posts']); // Advanced methods $user->hasDirectPermission('edit posts'); // Simplified in OpenFGA context $user->getDirectPermissions(); $user->getPermissionsViaRoles(); $user->hasExactRoles(['admin', 'editor']); ``` #### Limitations ##### Differences from Spatie 1. **No Role Hierarchy**: OpenFGA uses relationships instead of role inheritance 2. **No Guards on Relations**: OpenFGA relations don't have guard concepts 3. **No Permission Models**: Permissions are relationships, not database records 4. **Different Caching**: Uses OpenFGA's caching instead of Spatie's cache ##### Workarounds 1. **Role Hierarchy**: Use OpenFGA's relationship inheritance in your authorization model 2. **Complex Permissions**: Model complex scenarios using OpenFGA's relationship system 3. **Performance**: Leverage OpenFGA's built-in caching and batching ##### Migration Considerations 1. **Data Loss**: Some Spatie features don't have direct OpenFGA equivalents 2. **Performance Changes**: OpenFGA may have different performance characteristics 3. **API Changes**: Some advanced Spatie features may require code changes #### Best Practices ##### 1. Start with Compatibility Use the compatibility layer to migrate gradually: ```php // Start with compatibility if (config('spatie-compatibility.enabled')) { return $user->hasPermissionTo('edit posts'); } else { return OpenFga::check($user->authorizationUser(), 'editor', 'post:*'); } ``` ##### 2. Plan Your Authorization Model Design your OpenFGA model to match your Spatie structure: ```fga model schema 1.1 type user type organization relations define admin: [user] define editor: [user] or admin define member: [user] or editor type post relations define owner: [user] define editor: [user] or owner or editor from organization define viewer: [user] or editor ``` ##### 3. Test Thoroughly Create comprehensive tests: ```php class MigrationTest extends TestCase { public function test_spatie_compatibility_works() { $user = User::factory()->create(); $user->assignRole('admin'); $this->assertTrue($user->hasRole('admin')); $this->assertTrue($user->hasPermissionTo('manage users')); } } ``` ##### 4. Monitor Performance Compare performance before and after migration: ```php // Add timing to critical permission checks $start = microtime(true); $hasPermission = $user->hasPermissionTo('edit posts'); $duration = microtime(true) - $start; Log::info('Permission check duration', ['duration' => $duration]); ``` #### Support - **Issues**: Report compatibility issues on [GitHub](https://github.com/evansims/openfga-laravel/issues) - **Migration Help**: Use the `migration` label for migration-related questions - **Documentation**: Check the main [OpenFGA Laravel documentation](https://github.com/evansims/openfga-laravel/blob/main/../README.md) <!-- End of evansims/openfga-laravel/docs/spatie-compatibility.md --> <!-- Source: evansims/openfga-laravel/docs/testing.md --> ### Testing with OpenFGA Laravel This comprehensive guide covers how to effectively test applications that use OpenFGA for authorization, including unit tests, integration tests, and end-to-end testing strategies. #### Table of Contents - [Getting Started](#getting-started) - [Testing Utilities Overview](#testing-utilities-overview) - [Basic Testing](#basic-testing) - [Testing with Models](#testing-with-models) - [Advanced Testing Features](#advanced-testing-features) - [Testing Failures and Errors](#testing-failures-and-errors) - [Testing HTTP Requests](#testing-http-requests) - [Testing Events](#testing-events) - [Common Testing Scenarios](#common-testing-scenarios) - [Testing Utilities](#testing-utilities) - [Performance Testing](#performance-testing) - [Test Organization](#test-organization) - [Integration Testing](#integration-testing) - [Architectural Testing](#architectural-testing) - [Testing Best Practices](#testing-best-practices) - [Troubleshooting](#troubleshooting) #### Getting Started ##### Setting Up Tests The package provides a `FakesOpenFga` trait that replaces the real OpenFGA client with a fake implementation: ```php <?php namespace Tests\Feature; use Tests\TestCase; use OpenFGA\Laravel\Testing\FakesOpenFga; use App\Models\User; use App\Models\Document; class DocumentAuthorizationTest extends TestCase { use FakesOpenFga; protected function setUp(): void { parent::setUp(); // Initialize the fake OpenFGA implementation $this->fakeOpenFga(); } } ``` #### Testing Utilities Overview The package provides several testing utilities to make testing authorization logic straightforward and reliable: ##### 1. FakesOpenFga Trait Replaces the real OpenFGA service with a fake implementation for testing: ```php use OpenFGA\Laravel\Testing\FakesOpenFga; class MyTest extends TestCase { use FakesOpenFga; public function test_authorization() { $fake = $this->fakeOpenFga(); // Your test code here } } ``` ##### 2. CreatesPermissionData Trait Provides pre-built permission scenarios for common testing needs: ```php use OpenFGA\Laravel\Testing\CreatesPermissionData; class MyTest extends TestCase { use FakesOpenFga, CreatesPermissionData; public function test_blog_permissions() { $fake = $this->fakeOpenFga(); $data = $this->createBlogSystem($fake); // Test with pre-configured blog permission structure $this->assertTrue($fake->check($data['users']['admin'], 'admin', $data['blog'])); } } ``` ##### 3. AssertionHelper Class Provides specialized assertions for permission testing: ```php use OpenFGA\Laravel\Testing\AssertionHelper; // Assert user has specific permission AssertionHelper::assertUserHasPermission($fake, 'user:123', 'read', 'document:456'); // Assert user has any of multiple permissions AssertionHelper::assertUserHasAnyPermission($fake, 'user:123', ['read', 'write'], 'document:456'); // Assert user has all permissions AssertionHelper::assertUserHasAllPermissions($fake, 'user:123', ['read', 'write'], 'document:456'); ``` #### Basic Testing ##### Testing Permission Grants ```php public function test_user_can_be_granted_permission() { $user = User::factory()->create(); $document = Document::factory()->create(); // Grant permission OpenFga::grant("user:{$user->id}", 'editor', "document:{$document->id}"); // Assert permission was granted OpenFga::assertGranted("user:{$user->id}", 'editor', "document:{$document->id}"); // Verify the permission exists $this->assertTrue( OpenFga::check("user:{$user->id}", 'editor', "document:{$document->id}") ); } ``` ##### Testing Permission Checks ```php public function test_permission_check_returns_correct_result() { $user = User::factory()->create(); $document = Document::factory()->create(); // Initially, user should not have permission $this->assertFalse( OpenFga::check("user:{$user->id}", 'viewer', "document:{$document->id}") ); // Grant permission OpenFga::grant("user:{$user->id}", 'viewer', "document:{$document->id}"); // Now user should have permission $this->assertTrue( OpenFga::check("user:{$user->id}", 'viewer', "document:{$document->id}") ); // Assert the check was performed OpenFga::assertChecked("user:{$user->id}", 'viewer', "document:{$document->id}"); } ``` ##### Testing Permission Revocation ```php public function test_permission_can_be_revoked() { $user = User::factory()->create(); $document = Document::factory()->create(); // Grant and then revoke permission OpenFga::grant("user:{$user->id}", 'editor', "document:{$document->id}"); OpenFga::revoke("user:{$user->id}", 'editor', "document:{$document->id}"); // Assert permission was revoked OpenFga::assertRevoked("user:{$user->id}", 'editor', "document:{$document->id}"); // Verify permission no longer exists $this->assertFalse( OpenFga::check("user:{$user->id}", 'editor', "document:{$document->id}") ); } ``` #### Testing with Models ##### Testing Model Trait Methods ```php public function test_model_can_grant_permissions() { $user = User::factory()->create(); $document = Document::factory()->create(); // Use model method to grant permission $document->grant($user, 'editor'); // Assert using the expected format OpenFga::assertGranted( "user:{$user->id}", 'editor', $document->authorizationObject() ); // Verify through model method $this->assertTrue($document->check($user, 'editor')); } ``` ##### Testing Query Scopes ```php public function test_where_user_can_scope_filters_correctly() { $user = User::factory()->create(); $documents = Document::factory()->count(5)->create(); // Grant permissions to specific documents $documents[0]->grant($user, 'viewer'); $documents[2]->grant($user, 'viewer'); $documents[4]->grant($user, 'viewer'); // Mock the listObjects response OpenFga::shouldListObjects( "user:{$user->id}", 'viewer', 'document', [ $documents[0]->authorizationObject(), $documents[2]->authorizationObject(), $documents[4]->authorizationObject(), ] ); // Test the scope $viewableDocuments = Document::whereUserCan($user, 'viewer')->get(); $this->assertCount(3, $viewableDocuments); $this->assertTrue($viewableDocuments->contains($documents[0])); $this->assertTrue($viewableDocuments->contains($documents[2])); $this->assertTrue($viewableDocuments->contains($documents[4])); } ``` #### Advanced Testing Features ##### Mocking Specific Responses ```php public function test_handles_specific_permission_scenarios() { $user = User::factory()->create(); $document = Document::factory()->create(); // Mock specific check responses OpenFga::shouldCheck( "user:{$user->id}", 'viewer', "document:{$document->id}", true // Will return true ); OpenFga::shouldCheck( "user:{$user->id}", 'editor', "document:{$document->id}", false // Will return false ); // Test the mocked responses $this->assertTrue($document->check($user, 'viewer')); $this->assertFalse($document->check($user, 'editor')); } ``` ##### Testing Batch Operations ```php public function test_batch_write_operations() { $users = User::factory()->count(3)->create(); $document = Document::factory()->create(); // Perform batch write OpenFga::writeBatch( writes: [ ["user:{$users[0]->id}", 'viewer', "document:{$document->id}"], ["user:{$users[1]->id}", 'editor', "document:{$document->id}"], ["user:{$users[2]->id}", 'owner', "document:{$document->id}"], ] ); // Assert all permissions were granted OpenFga::assertBatchWritten([ ["user:{$users[0]->id}", 'viewer', "document:{$document->id}"], ["user:{$users[1]->id}", 'editor', "document:{$document->id}"], ["user:{$users[2]->id}", 'owner', "document:{$document->id}"], ]); // Verify individual permissions foreach ($users as $index => $user) { $relations = ['viewer', 'editor', 'owner']; $this->assertTrue( OpenFga::check("user:{$user->id}", $relations[$index], "document:{$document->id}") ); } } ``` ##### Testing with Contextual Tuples ```php public function test_contextual_permission_checks() { $user = User::factory()->create(); $document = Document::factory()->create(); $team = Team::factory()->create(); // Mock contextual check OpenFga::shouldCheckWithContext( "user:{$user->id}", 'viewer', "document:{$document->id}", [ ["user:{$user->id}", 'member', "team:{$team->id}"], ], true ); // Test contextual check $result = OpenFga::checkWithContext( "user:{$user->id}", 'viewer', "document:{$document->id}", [ ["user:{$user->id}", 'member', "team:{$team->id}"], ] ); $this->assertTrue($result); } ``` #### Testing Failures and Errors ##### Simulating Failures ```php public function test_handles_openfga_failures_gracefully() { $user = User::factory()->create(); $document = Document::factory()->create(); // Simulate a failure OpenFga::shouldFail('Connection timeout'); // Test that your application handles the failure try { $document->grant($user, 'editor'); $this->fail('Expected exception was not thrown'); } catch (\Exception $e) { $this->assertEquals('Connection timeout', $e->getMessage()); } } ``` ##### Testing Error Recovery ```php public function test_retries_failed_operations() { $user = User::factory()->create(); $document = Document::factory()->create(); // Fail first attempt, succeed on retry OpenFga::shouldFailTimes(1); // Your application should retry and succeed $document->grant($user, 'editor'); // Verify the permission was eventually granted OpenFga::assertGranted("user:{$user->id}", 'editor', "document:{$document->id}"); } ``` #### Testing HTTP Requests ##### Testing Protected Routes ```php public function test_protected_route_requires_permission() { $user = User::factory()->create(); $document = Document::factory()->create(); // Test without permission $response = $this->actingAs($user) ->get("/documents/{$document->id}/edit"); $response->assertForbidden(); // Grant permission OpenFga::grant("user:{$user->id}", 'editor', "document:{$document->id}"); // Test with permission $response = $this->actingAs($user) ->get("/documents/{$document->id}/edit"); $response->assertOk(); // Verify the permission was checked OpenFga::assertChecked("user:{$user->id}", 'editor', "document:{$document->id}"); } ``` ##### Testing API Endpoints ```php public function test_api_endpoint_authorization() { $user = User::factory()->create(); $document = Document::factory()->create(); $token = $user->createToken('test-token')->plainTextToken; // Grant API permission OpenFga::grant("user:{$user->id}", 'api_user', 'system:api'); OpenFga::grant("user:{$user->id}", 'editor', "document:{$document->id}"); // Make API request $response = $this->withHeaders([ 'Authorization' => "Bearer {$token}", 'Accept' => 'application/json', ])->putJson("/api/documents/{$document->id}", [ 'title' => 'Updated Title', 'content' => 'Updated content', ]); $response->assertOk(); $response->assertJson([ 'data' => [ 'id' => $document->id, 'title' => 'Updated Title', ], ]); } ``` #### Testing Events ##### Testing Permission Events ```php use Illuminate\Support\Facades\Event; use OpenFGA\Laravel\Events\PermissionGranted; use OpenFGA\Laravel\Events\PermissionChecked; public function test_events_are_dispatched() { Event::fake([ PermissionGranted::class, PermissionChecked::class, ]); $user = User::factory()->create(); $document = Document::factory()->create(); // Grant permission $document->grant($user, 'editor'); // Check permission $document->check($user, 'editor'); // Assert events were dispatched Event::assertDispatched(PermissionGranted::class, function ($event) use ($user, $document) { return $event->user === "user:{$user->id}" && $event->relation === 'editor' && $event->object === "document:{$document->id}"; }); Event::assertDispatched(PermissionChecked::class); } ``` #### Common Testing Scenarios The `CreatesPermissionData` trait provides pre-built scenarios for common authorization patterns. Here are some examples: ##### Blog System Testing ```php public function test_blog_author_permissions() { $fake = $this->fakeOpenFga(); $data = $this->createBlogSystem($fake); // Author can edit their own posts $this->assertTrue($fake->check($data['users']['author1'], 'author', $data['posts']['post1'])); // Author cannot edit other's posts $this->assertFalse($fake->check($data['users']['author1'], 'author', $data['posts']['post2'])); // Editor can edit all posts $this->assertTrue($fake->check($data['users']['editor'], 'editor', $data['blog'])); // Subscribers can read posts $this->assertTrue($fake->check($data['users']['subscriber'], 'reader', $data['posts']['post1'])); } ``` ##### File System Testing ```php public function test_file_system_permissions() { $fake = $this->fakeOpenFga(); $data = $this->createFileSystem($fake); // Users can access their home directories AssertionHelper::assertUserHasAllPermissions( $fake, $data['users']['user1'], ['read', 'write', 'execute'], $data['folders']['user1_home'] ); // Users cannot access other home directories AssertionHelper::assertUserDoesNotHavePermission( $fake, $data['users']['user1'], 'read', $data['folders']['user2_home'] ); // Shared files are accessible to all AssertionHelper::assertUserHasAccessToObjects( $fake, $data['users']['guest'], 'read', [$data['files']['shared_file']] ); } ``` ##### E-commerce Testing ```php public function test_ecommerce_permissions() { $fake = $this->fakeOpenFga(); $data = $this->createEcommerceSystem($fake); // Customers can view their own orders $this->assertTrue($fake->check($data['users']['customer1'], 'view', $data['orders']['order1'])); // Support can view all orders AssertionHelper::assertUserHasAccessToObjects( $fake, $data['users']['support'], 'view', array_values($data['orders']) ); // Vendors can manage their products $this->assertTrue($fake->check($data['users']['vendor1'], 'manage', $data['products']['product1'])); $this->assertFalse($fake->check($data['users']['vendor1'], 'manage', $data['products']['product3'])); } ``` ##### Organization Structure Testing ```php public function test_organization_hierarchy() { $fake = $this->fakeOpenFga(); $data = $this->createOrganizationStructure($fake); // CEO has admin access to organization $this->assertTrue($fake->check($data['users']['ceo'], 'admin', $data['organization'])); // Department managers can manage their departments $this->assertTrue($fake->check($data['users']['hr_manager'], 'manager', $data['departments']['hr'])); $this->assertFalse($fake->check($data['users']['hr_manager'], 'manager', $data['departments']['it'])); // Project contributors have appropriate access $this->assertTrue($fake->check($data['users']['developer'], 'contributor', $data['projects']['project1'])); } ``` ##### Complex Hierarchy Testing ```php public function test_nested_hierarchy_permissions() { $fake = $this->fakeOpenFga(); $data = $this->createNestedHierarchy($fake); // Super admin has top-level access $this->assertTrue($fake->check($data['users']['super_admin'], 'super_admin', $data['hierarchy']['company'])); // Managers have appropriate scope $this->assertTrue($fake->check($data['users']['dept_manager'], 'manager', $data['hierarchy']['department'])); $this->assertTrue($fake->check($data['users']['team_lead'], 'lead', $data['hierarchy']['team'])); // Contributors have project access AssertionHelper::assertUserHasAllPermissions( $fake, $data['users']['employee'], ['contributor'], $data['hierarchy']['project'] ); } ``` #### Testing Utilities ##### Creating Test Factories ```php use OpenFGA\Laravel\Testing\CreatesPermissionData; class PermissionFactory { use CreatesPermissionData; public function documentWithFullPermissions(User $user): Document { $document = Document::factory()->create(); $this->grantPermissions($document, $user, [ 'viewer', 'editor', 'owner', ]); return $document; } public function teamWithMembers(array $users, array $permissions): Team { $team = Team::factory()->create(); foreach ($users as $user) { foreach ($permissions as $permission) { $this->grant("user:{$user->id}", $permission, "team:{$team->id}"); } } return $team; } } ``` ##### Custom Assertions ```php trait CustomOpenFgaAssertions { protected function assertUserHasAllPermissions(User $user, Model $model, array $relations) { foreach ($relations as $relation) { $this->assertTrue( OpenFga::check("user:{$user->id}", $relation, $model->authorizationObject()), "User should have {$relation} permission on {$model->authorizationObject()}" ); } } protected function assertUserHasNoPermissions(User $user, Model $model, array $relations) { foreach ($relations as $relation) { $this->assertFalse( OpenFga::check("user:{$user->id}", $relation, $model->authorizationObject()), "User should not have {$relation} permission on {$model->authorizationObject()}" ); } } } ``` #### Performance Testing ##### Testing Query Performance ```php public function test_permission_checks_are_cached() { $user = User::factory()->create(); $document = Document::factory()->create(); // Grant permission OpenFga::grant("user:{$user->id}", 'viewer', "document:{$document->id}"); // First check - should hit OpenFGA $document->check($user, 'viewer'); // Reset check count OpenFga::resetCheckCount(); // Second check - should hit cache $document->check($user, 'viewer'); // Assert no additional checks were made $this->assertEquals(0, OpenFga::getCheckCount()); } ``` ##### Testing Batch Performance ```php public function test_batch_operations_are_efficient() { $users = User::factory()->count(100)->create(); $document = Document::factory()->create(); // Track operation count OpenFga::resetOperationCount(); // Perform batch grant $writes = $users->map(fn($user) => [ "user:{$user->id}", 'viewer', "document:{$document->id}" ])->toArray(); OpenFga::writeBatch(writes: $writes); // Should be 1 batch operation, not 100 individual operations $this->assertEquals(1, OpenFga::getBatchWriteCount()); $this->assertEquals(0, OpenFga::getIndividualWriteCount()); } ``` #### Test Organization ##### Using Test Traits ```php namespace Tests\Traits; trait SetsUpDocumentPermissions { protected function grantDocumentPermissions(User $user, Document $document, array $permissions) { foreach ($permissions as $permission) { OpenFga::grant("user:{$user->id}", $permission, "document:{$document->id}"); } } protected function createDocumentWithOwner(User $owner): Document { $document = Document::factory()->create(); OpenFga::grant("user:{$owner->id}", 'owner', "document:{$document->id}"); return $document; } } ``` ##### Test Data Builders ```php class DocumentTestBuilder { private Document $document; private array $permissions = []; public function __construct() { $this->document = Document::factory()->create(); } public function withViewer(User $user): self { $this->permissions[] = ["user:{$user->id}", 'viewer', $this->document->authorizationObject()]; return $this; } public function withEditor(User $user): self { $this->permissions[] = ["user:{$user->id}", 'editor', $this->document->authorizationObject()]; return $this; } public function build(): Document { if (!empty($this->permissions)) { OpenFga::writeBatch(writes: $this->permissions); } return $this->document; } } ``` #### Integration Testing ##### Testing with Real OpenFGA ```php namespace Tests\Integration; use Tests\TestCase; class RealOpenFgaTest extends TestCase { protected function setUp(): void { parent::setUp(); // Skip if not in integration test environment if (!env('RUN_INTEGRATION_TESTS')) { $this->markTestSkipped('Integration tests disabled'); } // Don't use fake - test against real OpenFGA // $this->fakeOpenFga(); // DON'T DO THIS } public function test_real_permission_check() { $user = User::factory()->create(); $document = Document::factory()->create(); // This will hit the real OpenFGA server $document->grant($user, 'editor'); // Verify against real server $this->assertTrue($document->check($user, 'editor')); // Clean up $document->revoke($user, 'editor'); } } ``` #### Architectural Testing The package includes architectural tests using Pest PHP's architecture plugin to ensure code quality and consistency. These tests help prevent architectural drift and maintain our design principles. ##### Running Architectural Tests ```bash # Run all architectural tests vendor/bin/pest tests/Architecture.php # Run specific architectural test vendor/bin/pest tests/Architecture.php --filter "strict types" # Run as part of the test suite vendor/bin/pest --testsuite architecture ``` ##### Architectural Rules The following architectural rules are enforced: ###### 1. **Strict Types Declaration** All PHP files must use strict types: ```php declare(strict_types=1); ``` ###### 2. **No Debug Functions** Debug functions like `dd()`, `dump()`, `var_dump()` are not allowed in production code. ###### 3. **Proper Namespace Structure** All classes must follow the `OpenFGA\Laravel` namespace convention. ###### 4. **Interface Conventions** - All contracts in `Contracts` directory must be interfaces - Traits must have the `Trait` suffix - Exceptions must extend the base `OpenFgaException` class ###### 5. **Laravel Integration** - Service providers must extend `Illuminate\Support\ServiceProvider` - Commands must extend `Illuminate\Console\Command` - Middleware must implement proper contracts ###### 6. **Dependency Injection** No hardcoded dependencies using `new` keyword (except for specific allowed namespaces like Exceptions, Models, etc.) ###### 7. **Method Standards** - All public methods must have return types - Check methods must return boolean values - Cache implementations must follow the get/put/forget pattern ###### 8. **Modern PHP Features** - Use constructor property promotion where applicable - Value objects should be immutable with readonly properties ###### 9. **Separation of Concerns** - Core abstracts should not depend on facades - Controllers should not directly use Eloquent models - Test helpers should only be used in test files ##### Adding New Architectural Rules To add new architectural rules, edit `tests/Architecture.php`: ```php arch('new rule description') ->expect('OpenFGA\Laravel\YourNamespace') ->toFollowYourRule() ->ignoring(['exceptions']); ``` ##### Architectural Test Examples ```php // Ensure all exceptions extend base exception arch('exceptions extend base exception') ->expect('OpenFGA\Laravel\Exceptions') ->toExtend('OpenFGA\Laravel\Exceptions\OpenFgaException') ->ignoring('OpenFGA\Laravel\Exceptions\OpenFgaException'); // Ensure no circular dependencies arch('no circular dependencies') ->expect('OpenFGA\Laravel\Abstracts') ->not->toDependOn('OpenFGA\Laravel\OpenFgaManager'); // Ensure consistent method naming arch('consistent method naming for checks') ->expect('OpenFGA\Laravel') ->toHaveMethodsThatStartWith('check') ->toReturnType('bool'); ``` #### Testing Best Practices ##### 1. Isolate Tests Always use the fake implementation for unit tests to ensure isolation: ```php public function test_isolated_permission_check() { $fake = $this->fakeOpenFga(); // Each test starts with a clean slate $this->assertNoPermissionChecks(); } ``` ##### 2. Test Permission Boundaries Test both positive and negative cases: ```php public function test_permission_boundaries() { $fake = $this->fakeOpenFga(); // Grant specific permission $fake->grant('user:123', 'read', 'document:456'); // Test granted permission $this->assertTrue($fake->check('user:123', 'read', 'document:456')); // Test different user (should fail) $this->assertFalse($fake->check('user:999', 'read', 'document:456')); // Test different permission (should fail) $this->assertFalse($fake->check('user:123', 'write', 'document:456')); // Test different object (should fail) $this->assertFalse($fake->check('user:123', 'read', 'document:999')); } ``` ##### 3. Test Complex Scenarios Use the permission data creators for complex scenarios: ```php public function test_complex_organization_permissions() { $fake = $this->fakeOpenFga(); $data = $this->createOrganizationStructure($fake); // Test CEO has admin access to organization $this->assertTrue($fake->check($data['users']['ceo'], 'admin', $data['organization'])); // Test department managers can manage their departments $this->assertTrue($fake->check($data['users']['hr_manager'], 'manager', $data['departments']['hr'])); $this->assertFalse($fake->check($data['users']['hr_manager'], 'manager', $data['departments']['it'])); } ``` ##### 4. Verify No Unexpected Checks Ensure your code doesn't make unnecessary permission checks: ```php public function test_no_redundant_permission_checks() { $fake = $this->fakeOpenFga(); // Perform action that should only check once $service = new DocumentService(); $service->getPublicDocuments(); // Assert exactly the expected number of checks $this->assertPermissionCheckCount(1); } ``` ##### 5. Test Cache Behavior Verify that caching works as expected: ```php public function test_permission_checks_are_cached() { $fake = $this->fakeOpenFga(); $user = User::factory()->create(); $document = Document::factory()->create(); // Grant permission $fake->grant("user:{$user->id}", 'viewer', "document:{$document->id}"); // First check $document->check($user, 'viewer'); // Second check should hit cache (if implemented) $document->check($user, 'viewer'); // Verify only one actual check was made $this->assertPermissionCheckCount(1); } ``` ##### 6. Use Descriptive Test Names Make test intentions clear: ```php public function test_document_owner_can_grant_editor_permissions_to_other_users() { // Test is self-documenting from the name } public function test_non_owner_cannot_grant_permissions_and_receives_403_error() { // Clear what should happen } ``` ##### 7. Group Related Tests Organize tests logically: ```php class DocumentPermissionTest extends TestCase { // All document-related permission tests } class UserRoleTest extends TestCase { // All user role tests } ``` #### Troubleshooting ##### Common Issues ###### 1. Fake Not Active ``` Error: OpenFGA fake is not active. Call fakeOpenFga() first. ``` **Solution**: Always call `$this->fakeOpenFga()` before making assertions: ```php public function test_something() { $fake = $this->fakeOpenFga(); // Must call this first // Now you can use assertions $this->assertNoPermissionChecks(); } ``` ###### 2. Permission Not Found ``` Error: Failed asserting that permission was granted ``` **Solution**: Ensure you're using exact string matching for user, relation, and object: ```php // Wrong - inconsistent formatting $fake->grant('user:123', 'read', 'document:456'); $this->assertPermissionGranted('user:123', 'read', 'document:457'); // Different ID // Correct $fake->grant('user:123', 'read', 'document:456'); $this->assertPermissionGranted('user:123', 'read', 'document:456'); ``` ###### 3. Unexpected Permission Checks Use the assertion helpers to debug what checks are being made: ```php public function test_debug_checks() { $fake = $this->fakeOpenFga(); // Your code here // Debug what checks were made $checks = $fake->getChecks(); dd($checks); // See all permission checks that occurred } ``` ##### Testing Environment Setup For consistent testing, configure your test environment: ```php // tests/TestCase.php protected function setUp(): void { parent::setUp(); // Always use fake in tests by default if (!app()->environment('production')) { $this->fakeOpenFga(); } } ``` ##### Test Data Management Create reusable test data: ```php // tests/Fixtures/PermissionFixtures.php class PermissionFixtures { public static function grantBasicDocumentPermissions(FakeOpenFga $fake, User $user, Document $document): void { $fake->grant("user:{$user->id}", 'read', "document:{$document->id}"); $fake->grant("user:{$user->id}", 'write', "document:{$document->id}"); } } ``` #### Next Steps - Optimize with [Performance Guide](https://github.com/evansims/openfga-laravel/blob/main/performance.md) - See [Troubleshooting Guide](https://github.com/evansims/openfga-laravel/blob/main/troubleshooting.md) - Check the [API Reference](https://github.com/evansims/openfga-laravel/blob/main/api-reference.md) - Review [Example Application](https://github.com/evansims/openfga-laravel) #### Additional Resources - [OpenFGA Documentation](https://openfga.dev/docs) - [Laravel Testing Documentation](https://laravel.com/docs/testing) - [PHPUnit Documentation](https://phpunit.de/documentation.html) - [Pest PHP Testing Framework](https://pestphp.com/) For more advanced testing scenarios and examples, see the `tests/` directory in this package for comprehensive test suites covering all features. <!-- End of evansims/openfga-laravel/docs/testing.md --> <!-- Source: evansims/openfga-laravel/docs/troubleshooting.md --> ### Troubleshooting Guide This guide helps you diagnose and resolve common issues when using OpenFGA Laravel. #### Common Issues ##### Connection Issues ###### Error: "Connection refused" or "Could not connect to OpenFGA server" **Symptoms:** - Operations fail with connection errors - Health check command fails - Timeout errors when making requests **Solutions:** 1. **Verify OpenFGA server is running:** ```bash # Check if server is accessible curl http://localhost:8080/healthz ``` 2. **Check your configuration:** ```bash php artisan openfga:debug ``` 3. **Verify environment variables:** ```bash # Check .env file OPENFGA_URL=http://localhost:8080 # Ensure URL is correct OPENFGA_STORE_ID=01ARZ3NDEKTSV4RRFFQ69G5FAV ``` 4. **Test with different timeout settings:** ```php // config/openfga.php 'http_options' => [ 'timeout' => 60, // Increase timeout 'connect_timeout' => 30, // Increase connection timeout ], ``` ###### Error: "Invalid store ID" or "Store not found" **Solutions:** 1. **Verify store exists:** ```bash # List stores using OpenFGA CLI fga store list ``` 2. **Create a new store if needed:** ```bash fga store create --name "my-app" ``` 3. **Update your .env with correct store ID:** ```env OPENFGA_STORE_ID=01GXSA8YR785C4FYS3C0RTG7B1 ``` ##### Authentication Issues ###### Error: "Unauthorized" or "Invalid credentials" **Solutions:** 1. **For API Token authentication:** ```env OPENFGA_AUTH_METHOD=api_token OPENFGA_API_TOKEN=your-valid-token-here ``` 2. **For OAuth2 Client Credentials:** ```env OPENFGA_AUTH_METHOD=client_credentials OPENFGA_CLIENT_ID=your-client-id OPENFGA_CLIENT_SECRET=your-client-secret OPENFGA_TOKEN_ISSUER=https://your-issuer.com OPENFGA_API_AUDIENCE=https://api.your-domain.com ``` 3. **Test authentication:** ```php // Create a test route Route::get('/test-auth', function () { try { $result = OpenFga::check('user:test', 'reader', 'document:test'); return 'Authentication successful'; } catch (\Exception $e) { return 'Authentication failed: ' . $e->getMessage(); } }); ``` ##### Permission Check Issues ###### Checks always return false **Solutions:** 1. **Verify permissions exist:** ```bash php artisan openfga:expand document:123 viewer ``` 2. **Check tuple format:** ```php // ✅ Correct format OpenFga::grant('user:123', 'viewer', 'document:456'); // ❌ Incorrect format OpenFga::grant('123', 'viewer', '456'); ``` 3. **Verify model exists:** ```bash php artisan openfga:debug # Check that Model ID is set ``` 4. **Clear cache if enabled:** ```bash php artisan cache:clear ``` ###### Inconsistent permission results **Solutions:** 1. **Check cache configuration:** ```php // Temporarily disable cache for testing 'cache' => [ 'enabled' => false, ], ``` 2. **Verify no duplicate tuples:** ```php // Check for duplicate grants $expansion = OpenFga::expand('document:123', 'viewer'); dd($expansion); // Inspect the results ``` 3. **Use consistent user identifiers:** ```php // Always use the same format $userId = "user:{$user->id}"; // Not sometimes 'user:123' and sometimes 'user:uuid-123' ``` ##### Model Integration Issues ###### Trait methods not working **Error:** "Call to undefined method grant()" **Solutions:** 1. **Ensure trait is properly imported:** ```php use OpenFGA\Laravel\Traits\HasAuthorization; class Document extends Model { use HasAuthorization; } ``` 2. **Clear Laravel's cache:** ```bash php artisan clear-compiled php artisan cache:clear composer dump-autoload ``` ###### whereUserCan scope returns empty results **Solutions:** 1. **Mock listObjects in tests:** ```php OpenFga::shouldListObjects( "user:{$user->id}", 'viewer', 'document', ['document:1', 'document:2', 'document:3'] ); ``` 2. **Check the authorization type:** ```php class Document extends Model { use HasAuthorization; // Ensure this matches your OpenFGA model protected function authorizationType(): string { return 'document'; // Must match OpenFGA type } } ``` ##### Middleware Issues ###### 403 Forbidden on all routes **Solutions:** 1. **Debug the middleware:** ```php // Add temporary logging class OpenFgaMiddleware { public function handle($request, $next, $relation, $object) { $user = "user:{$request->user()->id}"; \Log::info('Checking permission', [ 'user' => $user, 'relation' => $relation, 'object' => $object, ]); // ... rest of middleware } } ``` 2. **Check middleware parameters:** ```php // Correct format Route::middleware(['openfga:editor,document:{document}']) // Incorrect - missing object Route::middleware(['openfga:editor']) ``` 3. **Verify user is authenticated:** ```php Route::middleware(['auth', 'openfga:editor,document:{document}']) ``` ##### Queue Issues ###### Batch operations not processing **Solutions:** 1. **Ensure queue worker is running:** ```bash php artisan queue:work --queue=openfga ``` 2. **Check queue configuration:** ```env OPENFGA_QUEUE_ENABLED=true OPENFGA_QUEUE_CONNECTION=redis OPENFGA_QUEUE_NAME=openfga ``` 3. **Monitor failed jobs:** ```bash php artisan queue:failed ``` 4. **Process failed jobs:** ```bash php artisan queue:retry all ``` ##### Performance Issues ###### Slow permission checks **Solutions:** 1. **Enable caching:** ```env OPENFGA_CACHE_ENABLED=true OPENFGA_CACHE_STORE=redis OPENFGA_CACHE_TTL=300 ``` 2. **Use batch operations:** ```php // Instead of multiple checks $permissions = OpenFga::batchCheck([ ['user:123', 'viewer', 'document:1'], ['user:123', 'editor', 'document:1'], ['user:123', 'owner', 'document:1'], ]); ``` 3. **Monitor slow queries:** ```bash php artisan openfga:stats ``` 4. **Implement connection pooling:** ```php 'pool' => [ 'enabled' => true, 'min_connections' => 2, 'max_connections' => 10, ], ``` #### Debugging Tools ##### Enable Debug Mode ```php // In your .env file APP_DEBUG=true OPENFGA_LOGGING_ENABLED=true OPENFGA_LOG_CHANNEL=daily ``` ##### Use the Debug Command ```bash php artisan openfga:debug ``` Output shows: - Current configuration - Connection status - Store and model information - Cache and queue status ##### Enable Query Logging ```php // In a service provider or route OpenFga::enableQueryLog(); // After operations $queries = OpenFga::getQueryLog(); \Log::debug('OpenFGA Queries', $queries); ``` ##### Laravel Telescope Integration ```php // In TelescopeServiceProvider use OpenFGA\Laravel\Events\PermissionChecked; public function register() { Telescope::filter(function (IncomingEntry $entry) { if ($entry->type === 'event') { return !Str::startsWith($entry->content['name'], 'OpenFGA\\Laravel\\Events\\'); } return true; }); } ``` #### Testing Issues ##### Fake not working in tests **Solutions:** 1. **Ensure trait is used:** ```php use OpenFGA\Laravel\Testing\FakesOpenFga; class MyTest extends TestCase { use FakesOpenFga; protected function setUp(): void { parent::setUp(); $this->fakeOpenFga(); } } ``` 2. **Clear bindings between tests:** ```php protected function tearDown(): void { OpenFga::clearFake(); parent::tearDown(); } ``` ##### Assertions not working **Solutions:** 1. **Check assertion order:** ```php // First perform the action $document->grant($user, 'editor'); // Then assert OpenFga::assertGranted( "user:{$user->id}", 'editor', "document:{$document->id}" ); ``` 2. **Use correct format in assertions:** ```php // Match the exact format used in your code OpenFga::assertChecked( "user:{$user->id}", // Not just $user->id 'viewer', "document:{$document->id}" // Not just $document->id ); ``` #### Custom Exception Types OpenFGA Laravel provides typed exceptions for better error handling: ##### AuthorizationException Thrown when authorization operations fail. ```php use OpenFGA\Laravel\Exceptions\AuthorizationException; try { $allowed = OpenFga::check('user:123', 'viewer', 'document:456'); } catch (AuthorizationException $e) { // Specific handling for authorization failures \Log::error('Authorization failed: ' . $e->getMessage()); } ``` ##### ConnectionException Thrown when there are connection issues with OpenFGA. ```php use OpenFGA\Laravel\Exceptions\ConnectionException; try { $result = OpenFga::listStores(); } catch (ConnectionException $e) { // Handle connection issues if (str_contains($e->getMessage(), 'timed out')) { // Retry with longer timeout } } ``` ##### ModelNotFoundException Thrown when a specified authorization model cannot be found. ```php use OpenFGA\Laravel\Exceptions\ModelNotFoundException; try { $model = OpenFga::readAuthorizationModel($modelId); } catch (ModelNotFoundException $e) { // Model doesn't exist \Log::warning('Model not found: ' . $modelId); } ``` ##### StoreNotFoundException Thrown when a specified store cannot be found. ```php use OpenFGA\Laravel\Exceptions\StoreNotFoundException; try { $store = OpenFga::getStore($storeId); } catch (StoreNotFoundException $e) { // Store doesn't exist // Maybe create a new store or use default } ``` ##### InvalidTupleException Thrown when tuple format is invalid. ```php use OpenFGA\Laravel\Exceptions\InvalidTupleException; try { OpenFga::grant('invalid-format', 'viewer', 'document:123'); } catch (InvalidTupleException $e) { // Fix the tuple format // Should be 'type:id' format } ``` ##### ConnectionPoolException Thrown when connection pool operations fail. ```php use OpenFGA\Laravel\Exceptions\ConnectionPoolException; try { // Connection pool operations } catch (ConnectionPoolException $e) { // Handle pool exhaustion or connection failures } ``` ##### Base OpenFgaException All custom exceptions extend from `OpenFgaException`: ```php use OpenFGA\Laravel\Exceptions\OpenFgaException; try { // Any OpenFGA operation } catch (OpenFgaException $e) { // Catches any OpenFGA-related exception \Log::error('OpenFGA error: ' . $e->getMessage()); } ``` #### Error Messages Reference ##### "Result unwrapping failed" This occurs when using exception mode and an operation fails. **Solution:** ```php try { $result = OpenFga::check($user, $relation, $object); } catch (\OpenFGA\Laravel\Exceptions\AuthorizationException $e) { // Handle the error \Log::error('Authorization check failed', [ 'error' => $e->getMessage(), 'user' => $user, 'relation' => $relation, 'object' => $object, ]); } ``` ##### "Invalid tuple format" **Solution:** Ensure tuples follow the format: ```php // Correct formats 'user:123' 'group:admins' 'folder:projects#parent' 'team:engineering#member' // Incorrect formats '123' // Missing type prefix 'user-123' // Should use colon, not dash 'user::123' // Double colon ``` ##### "Model not found" **Solution:** 1. Check model ID in configuration 2. Verify model exists in the store 3. Try without specifying model ID (uses latest) #### Getting Help ##### 1. Check Logs ```bash # Laravel logs tail -f storage/logs/laravel.log # OpenFGA specific logs tail -f storage/logs/openfga.log ``` ##### 2. Enable Verbose Output ```bash php artisan openfga:check user:123 viewer document:456 -vvv ``` ##### 3. Community Support - **GitHub Issues**: Report bugs or request features - **Discussions**: Ask questions and share solutions - **Stack Overflow**: Tag questions with `openfga` and `laravel` ##### 4. Professional Support For enterprise support, contact the OpenFGA team through official channels. #### Prevention Tips 1. **Always validate configuration on deployment:** ```bash php artisan openfga:debug ``` 2. **Use health checks:** ```php Route::get('/health/openfga', function () { try { OpenFga::check('user:health', 'check', 'system:health'); return response()->json(['status' => 'healthy']); } catch (\Exception $e) { return response()->json(['status' => 'unhealthy', 'error' => $e->getMessage()], 503); } }); ``` 3. **Monitor performance:** ```bash php artisan openfga:stats ``` 4. **Keep dependencies updated:** ```bash composer update evansms/openfga-laravel ``` <!-- End of evansims/openfga-laravel/docs/troubleshooting.md --> <!-- Source: evansims/openfga-laravel/docs/webhooks.md --> ### Webhooks OpenFGA Laravel supports both incoming and outgoing webhooks for real-time authorization updates. #### Table of Contents - [Overview](#overview) - [Incoming Webhooks](#incoming-webhooks) - [Outgoing Webhooks](#outgoing-webhooks) - [Configuration](#configuration) - [Security](#security) - [Testing](#testing) - [Troubleshooting](#troubleshooting) #### Overview The webhook system allows your application to: - **Receive notifications** from OpenFGA when authorization data changes - **Send notifications** to external systems when permissions are modified - **Invalidate caches** automatically for better data consistency - **React in real-time** to authorization changes #### Incoming Webhooks Incoming webhooks allow OpenFGA to notify your application when authorization data changes. ##### Setup 1. **Enable webhooks** in your `.env` file: ```env OPENFGA_WEBHOOKS_ENABLED=true OPENFGA_WEBHOOK_SECRET=your-secret-key ``` 2. **Configure OpenFGA** to send webhooks to your endpoint: ``` https://your-app.com/openfga/webhook ``` 3. **Ensure the route is accessible** from OpenFGA's servers. ##### Webhook Events The package handles the following webhook events from OpenFGA: ###### Authorization Model Write Triggered when the authorization model is updated. ```json { "type": "authorization_model_write", "data": { "store_id": "01HQMVAH3R8X123456789", "model_id": "01HQMVAH3R8X987654321" } } ``` ###### Tuple Write Triggered when a relationship tuple is created or updated. ```json { "type": "tuple_write", "data": { "user": "user:123", "relation": "editor", "object": "document:456" } } ``` ###### Tuple Delete Triggered when a relationship tuple is deleted. ```json { "type": "tuple_delete", "data": { "user": "user:123", "relation": "editor", "object": "document:456" } } ``` ##### Handling Webhooks When a webhook is received, the package automatically: 1. **Verifies the signature** (if configured) 2. **Validates the payload** 3. **Invalidates relevant caches** 4. **Dispatches events** for custom handling You can listen to webhook events in your application: ```php use OpenFGA\Laravel\Events\WebhookReceived; use Illuminate\Support\Facades\Event; Event::listen(WebhookReceived::class, function (WebhookReceived $event) { Log::info('Webhook received', [ 'type' => $event->type, 'data' => $event->data, ]); // Custom handling based on webhook type match ($event->type) { 'tuple_write' => $this->handleTupleWrite($event->data), 'tuple_delete' => $this->handleTupleDelete($event->data), default => null, }; }); ``` #### Outgoing Webhooks Outgoing webhooks notify external systems when permissions change in your application. ##### Configuration Configure endpoints in `config/openfga.php`: ```php 'webhooks' => [ 'enabled' => env('OPENFGA_WEBHOOKS_ENABLED', false), 'timeout' => env('OPENFGA_WEBHOOK_TIMEOUT', 5), 'retries' => env('OPENFGA_WEBHOOK_RETRIES', 3), 'endpoints' => [ 'audit_log' => [ 'url' => 'https://audit.example.com/webhook', 'headers' => [ 'Authorization' => 'Bearer ' . env('AUDIT_WEBHOOK_TOKEN'), 'X-Service-Name' => 'openfga-laravel', ], 'events' => ['permission.granted', 'permission.revoked'], 'active' => true, ], 'slack' => [ 'url' => env('SLACK_WEBHOOK_URL'), 'events' => ['*'], // All events 'active' => true, ], ], ], ``` ##### Webhook Payload Outgoing webhooks send the following payload: ```json { "event": "permission.granted", "timestamp": "2024-01-20T10:30:00Z", "data": { "user": "user:123", "relation": "editor", "object": "document:456", "action": "grant", "metadata": { "ip": "192.168.1.100", "user_agent": "Mozilla/5.0...", "request_id": "abc-123" } }, "environment": "production", "application": "My App" } ``` ##### Programmatic Management You can manage webhooks programmatically: ```php use OpenFGA\Laravel\Webhooks\WebhookManager; $webhooks = app(WebhookManager::class); // Register a new webhook $webhooks->register('custom', 'https://example.com/hook', [ 'permission.granted', 'permission.revoked' ], [ 'Authorization' => 'Bearer token', ]); // Disable a webhook temporarily $webhooks->disable('custom'); // Enable a webhook $webhooks->enable('custom'); // Remove a webhook $webhooks->unregister('custom'); // Get all registered webhooks $registered = $webhooks->getWebhooks(); ``` #### Configuration ##### Environment Variables ```env # Enable/disable webhooks OPENFGA_WEBHOOKS_ENABLED=true # Incoming webhook secret for signature verification OPENFGA_WEBHOOK_SECRET=your-secret-key # Outgoing webhook configuration OPENFGA_WEBHOOK_TIMEOUT=5 OPENFGA_WEBHOOK_RETRIES=3 OPENFGA_WEBHOOK_SEND_CHECKS=false # Example outgoing webhook AUDIT_WEBHOOK_URL=https://audit.example.com/webhook AUDIT_WEBHOOK_TOKEN=your-audit-token ``` ##### Configuration Options | Option | Description | Default | |--------|-------------|---------| | `webhooks.enabled` | Enable/disable webhook functionality | `false` | | `webhooks.secret` | Secret for verifying incoming webhooks | `null` | | `webhooks.timeout` | Timeout for outgoing webhook requests (seconds) | `5` | | `webhooks.retries` | Number of retries for failed webhook requests | `3` | | `webhooks.send_check_events` | Send webhooks for permission check events | `false` | #### Security ##### Signature Verification Incoming webhooks are verified using HMAC-SHA256 signatures: 1. OpenFGA signs the payload with your shared secret 2. The signature is sent in the `X-OpenFGA-Signature` header 3. The package verifies the signature before processing ##### Best Practices 1. **Always use HTTPS** for webhook endpoints 2. **Set a strong webhook secret** and rotate it regularly 3. **Validate IP addresses** if OpenFGA provides a fixed IP range 4. **Implement rate limiting** to prevent abuse 5. **Log all webhook activity** for auditing 6. **Use authentication tokens** for outgoing webhooks #### Testing ##### Testing Incoming Webhooks ```php use Illuminate\Support\Facades\Event; use OpenFGA\Laravel\Events\WebhookReceived; public function test_incoming_webhook_invalidates_cache() { Event::fake(); // Enable webhooks config(['openfga.webhooks.enabled' => true]); config(['openfga.webhooks.secret' => 'test-secret']); // Create signed payload $payload = [ 'type' => 'tuple_write', 'data' => [ 'user' => 'user:123', 'relation' => 'editor', 'object' => 'document:456', ], ]; $signature = hash_hmac('sha256', json_encode($payload), 'test-secret'); // Send webhook request $response = $this->postJson('/openfga/webhook', $payload, [ 'X-OpenFGA-Signature' => $signature, ]); $response->assertOk(); Event::assertDispatched(WebhookReceived::class); } ``` ##### Testing Outgoing Webhooks ```php use Illuminate\Support\Facades\Http; use OpenFGA\Laravel\Events\PermissionChanged; public function test_outgoing_webhook_sent_on_permission_change() { Http::fake(); // Configure webhook config([ 'openfga.webhooks.enabled' => true, 'openfga.webhooks.endpoints' => [ 'test' => [ 'url' => 'https://example.com/webhook', 'events' => ['permission.granted'], 'active' => true, ], ], ]); // Trigger permission change Event::dispatch(new PermissionChanged( user: 'user:123', relation: 'editor', object: 'document:456', action: 'grant' )); // Assert webhook was sent Http::assertSent(function ($request) { return $request->url() === 'https://example.com/webhook' && $request['event'] === 'permission.granted'; }); } ``` #### Troubleshooting ##### Webhooks Not Being Received 1. **Check if webhooks are enabled**: ```php dd(config('openfga.webhooks.enabled')); ``` 2. **Verify the route is registered**: ```bash php artisan route:list | grep webhook ``` 3. **Check logs for errors**: ```bash tail -f storage/logs/laravel.log | grep webhook ``` ##### Signature Verification Failing 1. **Ensure secrets match** between OpenFGA and your application 2. **Check header name** - should be `X-OpenFGA-Signature` 3. **Verify payload encoding** - should be raw JSON ##### Cache Not Being Invalidated 1. **Check cache configuration**: ```php dd(config('openfga.cache.enabled')); ``` 2. **Verify cache driver supports tags** (if using tagged cache) 3. **Check webhook processing logs** ##### Outgoing Webhooks Failing 1. **Check endpoint URL** is correct and accessible 2. **Verify authentication headers** are properly set 3. **Review timeout settings** - increase if needed 4. **Check retry configuration** ##### Debug Mode Enable detailed logging for webhook debugging: ```php // In your AppServiceProvider use Illuminate\Support\Facades\Log; use OpenFGA\Laravel\Events\WebhookReceived; Event::listen(WebhookReceived::class, function ($event) { Log::channel('webhook')->info('Webhook received', [ 'type' => $event->type, 'data' => $event->data, 'headers' => request()->headers->all(), ]); }); ``` #### Next Steps - Review [Caching](https://github.com/evansims/openfga-laravel/blob/main/caching.md) for cache invalidation strategies - See [Events](https://github.com/evansims/openfga-laravel/blob/main/events.md) for handling permission changes - Check [Security](https://github.com/evansims/openfga-laravel/blob/main/security.md) for additional security considerations <!-- End of evansims/openfga-laravel/docs/webhooks.md -->

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/evansims/openfga-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server