Layered Permission Checks with Context-Aware Authorization

๐Ÿ“– 3 minutes read




Layered Permission Checks with Context-Aware Authorization

Layered Permission Checks with Context-Aware Authorization

Role-based permissions are great for simple features. But what if you need different access rules based on the state of the data being accessed?

Don’t just check roles. Layer your permission checks with context-aware logic.

The Problem

You have an “Advanced Settings” section. Originally, only super admins could see it:

if (auth()->user()->hasRole('super.admin')) {
    // Show advanced settings
}

But now you need more complex rules:

  • Tech team should always see it (for debugging)
  • Regular admins can see it for draft records
  • Only executives can see it for published records

Trying to cram all this into role checks gets messy fast.

The Solution: Layered Authorization

Break authorization into layers:

  1. Base permission: Does the user have the feature enabled at all?
  2. Context check: What state is the record in?
  3. Role check: Does their role allow access for this record state?
  4. Escape hatch: Always allow admin users (for debugging)

In code:

function canViewAdvancedSettings(User $user, Report $report): bool
{
    // Layer 1: Tech team bypass (debugging)
    if ($user->hasRole('tech.team')) {
        return true;
    }
    
    // Layer 2: Base permission check
    if (!$user->hasPermission('view.advanced.settings')) {
        return false;
    }
    
    // Layer 3: Context-aware role check
    if ($report->isPublished()) {
        // Published reports: only executives
        return $user->hasRole('executive');
    }
    
    // Default: any user with base permission can view drafts
    return true;
}

Why This Works

  • Tech team always gets in: They need to debug production issues
  • Base permission as gate: Users without the permission never see the feature
  • Context-aware rules: Different record states have different access requirements
  • Explicit defaults: Clear fallback behavior when special cases don’t apply

Blade Integration

Use this in your blade templates:

@php
    $showAdvanced = (function($user, $report) {
        if ($user->hasRole('tech.team')) {
            return true;
        }
        
        if (!$user->hasPermission('view.advanced.settings')) {
            return false;
        }
        
        if ($report->isPublished()) {
            return $user->hasRole('executive');
        }
        
        return true;
    })(auth()->user(), $report);
@endphp

@if($showAdvanced)
    <div class="advanced-settings">
        {{-- Advanced settings UI --}}
    </div>
@endif

Alternative: Laravel Policies

For reusable logic, extract this to a policy method:

// app/Policies/ReportPolicy.php
public function viewAdvancedSettings(User $user, Report $report): bool
{
    if ($user->hasRole('tech.team')) {
        return true;
    }
    
    if (!$user->hasPermission('view.advanced.settings')) {
        return false;
    }
    
    if ($report->isPublished()) {
        return $user->hasRole('executive');
    }
    
    return true;
}

Then use @can in your blade templates:

@can('viewAdvancedSettings', $report)
    <div class="advanced-settings">
        {{-- Advanced settings UI --}}
    </div>
@endcan

Key Insight

Don’t try to solve complex authorization with just roles. Layer your checks:

  1. Start with escape hatches (tech/admin bypass)
  2. Check base permissions
  3. Apply context-aware rules (record state, user attributes, etc.)
  4. Provide clear defaults

This pattern scales much better than trying to create a role for every combination of access rules.

Category: Laravel | Keywords: Laravel, permissions, authorization, role-based, context-aware, security


Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *