Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Authentication & Authorization/Authorization

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

  1. Roles & Permissions
  2. How Authorization Works
  3. Permission Caching
  4. Permission Matrix UI
  5. Policies
  6. Gates
  7. Middleware
  8. Technical Architecture
  9. Related Features

Roles & Permissions

Admin Role

Administrators have complete access to all system resources.

ResourcePermissions
ClientsView all, create, update, delete
ProjectsView all, create, update, delete
FilesView all, upload, download, delete
InvoicesView all, create, update, delete
Activity LogsView all logs
ReportsGenerate any report
SettingsFull system configuration

Client Role

Client users have limited access scoped to their associated client organizations.

ResourcePermissions
ClientsView only their own client profile
ProjectsView projects for their client
FilesView/download client-visible files, upload files
InvoicesView invoices for their client
Activity LogsView their own activity only
ReportsLimited 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:

  1. Before Policy Evaluation: Gate checks cache for existing permission result
  2. After Policy Evaluation: Results are automatically cached for future checks
  3. 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

  1. 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
  2. Cache Management

    • View cached permission count
    • Clear all permission caches with one click
  3. Resource Documentation

    • Policy class references
    • Permission descriptions and notes

API Endpoints

EndpointMethodDescription
/admin/settings/permissionsGETView permission matrix page
/admin/settings/permissions/matrixGETGet matrix data as JSON
/admin/settings/permissions/clear-cachePOSTClear 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.

MethodAdminClient 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.

MethodAdminClient 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.

MethodAdminClient 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.

MethodAdminClient 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.

MethodAdminClient User
viewAny✅ All logs✅ Own activity only
view✅ Any log✅ Own activity only

Location: app/Policies/ActivityLogPolicy.php

Additional Policies

PolicyPurpose
SavedFilterPolicySaved filter access
ScheduledReportPolicyScheduled report access
ConversationPolicyMessage conversation access
TimesheetPolicyTimesheet access
TimeEntryPolicyTime entry access
DocumentRequestPolicyDocument request access
DeliverableApprovalPolicyDeliverable approval access
ClientUploadPolicyClient 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

TableColumnPurpose
usersroleUser 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']);
    });

Dependencies

FeatureRelationship
AuthenticationProvides user identity
Database SchemaUser roles and client associations

Complementary Features

FeatureDescription
Activity LoggingLogs authorization events
Audit ComplianceCompliance tracking

Best Practices

For Developers

  1. Always authorize in controllers

    $this->authorize('view', $project);
    
  2. Use policies for model-based authorization

    @can('update', $project)
    
  3. Use gates for simple role checks

    Gate::authorize('admin');
    
  4. Never trust client-side checks - Always verify on server

  5. Scope queries for client users

    $projects = $user->isAdmin()
        ? Project::all()
        : Project::whereIn('client_id', $user->clientIds())->get();
    

Troubleshooting

IssueSolution
403 ForbiddenCheck user role and policy permissions
Client sees admin dataVerify scoping in controller queries
Policy not workingEnsure policy is registered and model matches
Gate undefinedCheck gate is defined in AppServiceProvider

See Also