Authorization Guide
Last Updated: 2026-01-13 Status: Implemented Plan Reference: 004-authorization-role-system.md, 069-authorization-improvement.md
Overview
The authorization system implements role-based access control (RBAC) using Laravel Policies and Gates. It defines two primary roles—Admin and Client—with distinct permissions. Admins have full system access while clients can only access resources belonging to their associated client organizations.
Table of Contents
- Roles & Permissions
- How Authorization Works
- Permission Caching
- Permission Matrix UI
- Policies
- Gates
- Middleware
- Technical Architecture
- Related Features
Roles & Permissions
Admin Role
Administrators have complete access to all system resources.
| Resource | Permissions |
|---|---|
| Clients | View all, create, update, delete |
| Projects | View all, create, update, delete |
| Files | View all, upload, download, delete |
| Invoices | View all, create, update, delete |
| Activity Logs | View all logs |
| Reports | Generate any report |
| Settings | Full system configuration |
Client Role
Client users have limited access scoped to their associated client organizations.
| Resource | Permissions |
|---|---|
| Clients | View only their own client profile |
| Projects | View projects for their client |
| Files | View/download client-visible files, upload files |
| Invoices | View invoices for their client |
| Activity Logs | View their own activity only |
| Reports | Limited to their client's data |
How Authorization Works
Role Check on User Model
// Check if user is an admin
if ($user->isAdmin()) {
// Full access
}
// Check if user is a client
if ($user->isClient()) {
// Scoped access
}
// Check if user belongs to a specific client
if ($user->belongsToClient($client)) {
// Has access to this client
}
Using Policies in Controllers
// Authorize in controller method
public function show(Client $client)
{
$this->authorize('view', $client);
return view('clients.show', compact('client'));
}
// Authorize with Gate
public function index()
{
Gate::authorize('admin');
return Client::all();
}
Using Policies in Blade Views
{{-- Check specific permission --}}
@can('update', $project)
<a href="{{ route('admin.projects.edit', $project) }}">Edit</a>
@endcan
{{-- Check admin role --}}
@can('admin')
<a href="{{ route('admin.settings') }}">Settings</a>
@endcan
{{-- Alternative syntax --}}
@cannot('delete', $invoice)
<span>Cannot delete</span>
@endcannot
Permission Caching
Permission results are cached to improve performance and reduce database queries during authorization checks.
How It Works
The PermissionCacheService caches permission check results using Laravel's Cache facade:
// Permission results are automatically cached after policy evaluation
// Cache TTL: 1 hour (3600 seconds)
// Manual cache operations
$cacheService = app(PermissionCacheService::class);
// Get cached permission (returns null if not cached)
$result = $cacheService->getCachedPermission($user, 'viewAny', Client::class);
// Cache a permission result
$cacheService->cachePermission($user, 'view', $client, true);
// Clear permissions for a specific user
$cacheService->clearUserPermissions($user);
// Clear all permission caches
$cacheService->clearAllPermissions();
// Get cache statistics
$stats = $cacheService->getStats();
// Returns: ['cached_permissions' => 42, 'cache_ttl_seconds' => 3600]
Automatic Cache Integration
Permission caching is integrated with Laravel Gate:
- Before Policy Evaluation: Gate checks cache for existing permission result
- After Policy Evaluation: Results are automatically cached for future checks
- Cache Key Format:
permissions:{user_id}:{ability}:{model_key}
Cache Invalidation
Permission caches are automatically invalidated when:
- User role changes (via
UserObserver) - User is deleted
- Admin manually clears cache from Permission Matrix UI
// UserObserver handles automatic invalidation
class UserObserver
{
public function updating(User $user): void
{
if ($user->isDirty('role')) {
$this->permissionCache->clearUserPermissions($user);
}
}
}
Testing Consideration
Permission caching is disabled in testing environment to ensure predictable test results:
// In AppServiceProvider::registerPermissionCaching()
if ($this->app->environment('testing')) {
return; // Skip caching in tests
}
Permission Matrix UI
Administrators can view and manage permissions through a visual matrix interface.
Accessing the Permission Matrix
URL: /admin/settings/permissions
Route: admin.settings.permissions.index
Features
-
Visual Permission Matrix
- View all resources and their policy permissions
- See admin vs. client role permissions at a glance
- View notes explaining specific permission behaviors
-
Cache Management
- View cached permission count
- Clear all permission caches with one click
-
Resource Documentation
- Policy class references
- Permission descriptions and notes
API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/admin/settings/permissions | GET | View permission matrix page |
/admin/settings/permissions/matrix | GET | Get matrix data as JSON |
/admin/settings/permissions/clear-cache | POST | Clear permission cache |
Permission Matrix Data
// Controller returns structured permission data
$resources = [
'Clients' => [
'description' => 'Client/Company management',
'policy' => 'App\\Policies\\ClientPolicy',
'permissions' => [
'viewAny' => ['admin' => true, 'client' => true, 'notes' => 'Clients see only their own'],
'create' => ['admin' => true, 'client' => false, 'notes' => ''],
// ...
],
],
// ...
];
Policies
ClientPolicy
Controls access to client resources.
| Method | Admin | Client User |
|---|---|---|
viewAny | ✅ All clients | ❌ Not allowed |
view | ✅ Any client | ✅ Own client only |
create | ✅ | ❌ |
update | ✅ | ❌ |
delete | ✅ | ❌ |
Location: app/Policies/ClientPolicy.php
ProjectPolicy
Controls access to project resources.
| Method | Admin | Client User |
|---|---|---|
viewAny | ✅ All projects | ✅ Own client's projects |
view | ✅ Any project | ✅ Own client's projects |
create | ✅ | ❌ |
update | ✅ | ❌ |
delete | ✅ | ❌ |
Location: app/Policies/ProjectPolicy.php
ProjectFilePolicy
Controls access to project files.
| Method | Admin | Client User |
|---|---|---|
viewAny | ✅ All files | ✅ Client-visible files |
view | ✅ Any file | ✅ Client-visible files |
create | ✅ | ✅ For own projects |
download | ✅ Any file | ✅ Client-visible files |
delete | ✅ | ✅ Own uploads only |
Location: app/Policies/ProjectFilePolicy.php
InvoicePolicy
Controls access to invoices.
| Method | Admin | Client User |
|---|---|---|
viewAny | ✅ All invoices | ✅ Own client's invoices |
view | ✅ Any invoice | ✅ Own client's invoices |
create | ✅ | ❌ |
update | ✅ | ❌ |
delete | ✅ | ❌ |
Location: app/Policies/InvoicePolicy.php
ActivityLogPolicy
Controls access to activity logs.
| Method | Admin | Client User |
|---|---|---|
viewAny | ✅ All logs | ✅ Own activity only |
view | ✅ Any log | ✅ Own activity only |
Location: app/Policies/ActivityLogPolicy.php
Additional Policies
| Policy | Purpose |
|---|---|
SavedFilterPolicy | Saved filter access |
ScheduledReportPolicy | Scheduled report access |
ConversationPolicy | Message conversation access |
TimesheetPolicy | Timesheet access |
TimeEntryPolicy | Time entry access |
DocumentRequestPolicy | Document request access |
DeliverableApprovalPolicy | Deliverable approval access |
ClientUploadPolicy | Client upload access |
Gates
Gates provide simple authorization checks without model binding.
Available Gates
// Check if user is an admin
Gate::allows('admin')
// Check if user can access a specific client
Gate::allows('access-client', $client)
Using Gates
// In controller
if (Gate::allows('admin')) {
// Admin-only logic
}
// With authorization
Gate::authorize('admin');
// In Blade
@can('admin')
<a href="/admin/settings">Admin Settings</a>
@endcan
Middleware
EnsureUserIsAdmin
Protects routes that require admin access.
Alias: admin
Location: app/Http/Middleware/EnsureUserIsAdmin.php
// Usage in routes
Route::middleware('admin')->group(function () {
Route::get('/admin/settings', [SettingsController::class, 'index']);
});
Behavior:
- Returns 403 JSON response for API requests
- Aborts with 403 for web requests
EnsureClientAccess
Ensures client users can only access their own resources.
Location: app/Http/Middleware/EnsureClientAccess.php
Technical Architecture
Middleware Registration
Middleware is registered in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
'client.access' => \App\Http\Middleware\EnsureClientAccess::class,
]);
})
Policy Registration
Policies are auto-discovered by Laravel, but explicitly registered in AppServiceProvider:
// app/Providers/AppServiceProvider.php
Gate::policy(Client::class, ClientPolicy::class);
Gate::policy(Project::class, ProjectPolicy::class);
Gate::policy(ProjectFile::class, ProjectFilePolicy::class);
Gate::policy(Invoice::class, InvoicePolicy::class);
Gate::policy(ActivityLog::class, ActivityLogPolicy::class);
Gate Definitions
// app/Providers/AppServiceProvider.php
Gate::define('admin', fn($user) => $user->isAdmin());
Gate::define('access-client', fn($user, $client) =>
$user->isAdmin() || $user->belongsToClient($client)
);
User Model Helpers
// app/Models/User.php
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isClient(): bool
{
return $this->role === 'client';
}
public function belongsToClient(Client $client): bool
{
return $this->clients()->where('client_id', $client->id)->exists();
}
Database Schema
| Table | Column | Purpose |
|---|---|---|
users | role | User role (admin/client) |
client_user | - | Pivot table linking users to clients |
Route Protection Examples
Admin-Only Routes
Route::prefix('admin')
->middleware(['auth', 'verified', 'admin'])
->name('admin.')
->group(function () {
Route::resource('clients', Admin\ClientController::class);
Route::resource('projects', Admin\ProjectController::class);
});
Client Portal Routes
Route::prefix('portal')
->middleware(['auth', 'verified'])
->name('portal.')
->group(function () {
Route::get('/dashboard', [PortalController::class, 'dashboard']);
Route::get('/projects', [PortalController::class, 'projects']);
});
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Authentication | Provides user identity |
| Database Schema | User roles and client associations |
Complementary Features
| Feature | Description |
|---|---|
| Activity Logging | Logs authorization events |
| Audit Compliance | Compliance tracking |
Best Practices
For Developers
-
Always authorize in controllers
$this->authorize('view', $project); -
Use policies for model-based authorization
@can('update', $project) -
Use gates for simple role checks
Gate::authorize('admin'); -
Never trust client-side checks - Always verify on server
-
Scope queries for client users
$projects = $user->isAdmin() ? Project::all() : Project::whereIn('client_id', $user->clientIds())->get();
Troubleshooting
| Issue | Solution |
|---|---|
| 403 Forbidden | Check user role and policy permissions |
| Client sees admin data | Verify scoping in controller queries |
| Policy not working | Ensure policy is registered and model matches |
| Gate undefined | Check gate is defined in AppServiceProvider |
See Also
- Authentication Guide - User login system
- Security Guide - Security best practices
- Activity Logging - Audit trail