Files
timebank-cc-public/references/MULTI_GUARD_PERMISSION_SYSTEM_FIXES_2026-01-03.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

9.6 KiB

Multi-Guard Permission System Fixes - 2026-01-03

Summary

Fixed critical permission checking errors in the multi-guard authentication system that were causing "Permission does not exist for guard" errors when users attempted to switch profiles or edit Organization/Bank profiles. All authorization security tests now passing (60/60).

Issues Identified

1. CanOnWebGuard Middleware - Strict Permission Checking

File: app/Http/Middleware/CanOnWebGuard.php:13

Problem: Used hasPermissionTo($permission, 'web') which strictly validates that the permission record exists for the specific guard. This threw exceptions for permissions that exist as Gates but not as database records for the 'web' guard.

Impact:

  • Routes with user.can:manage organizations middleware failed
  • Organization/Bank profile editing pages returned 500 errors
  • Session expiry on profile pages caused crashes

Solution: Changed to can($permission) method which works with both database permissions and Gate definitions, providing flexibility for the multi-guard system.

2. Gate Definitions - Missing Error Handling

File: app/Providers/AuthServiceProvider.php:65-99

Problem: Gate definitions for 'manage organizations', 'manage banks', 'manage admins' didn't have proper exception handling and instanceof checks.

Impact: Gates could throw exceptions when called with non-User instances (Organization/Bank/Admin models).

Solution: Added try-catch blocks and instanceof checks to ensure Gates only work with User models and gracefully return false for errors.

3. @usercan Blade Directive - Cross-Guard Permission Checking

File: app/Providers/AppServiceProvider.php:106-130

Problem: Directive checked active profile's permissions instead of the web user's permissions. In the multi-guard system, all permissions are stored on the 'web' guard only.

Impact: Navigation links disappeared when switched to Organization/Bank/Admin profiles because those models don't have permission records.

Solution: Rewrote directive to always check the web user's permissions using can() with proper error handling.

4. Profile Form Components - Middleware and Permission Checks

Files:

  • app/Http/Livewire/ProfileOrganization/UpdateProfileOrganizationForm.php
  • app/Http/Livewire/ProfileBank/UpdateProfileBankForm.php
  • app/Http/Livewire/ProfileUser/UpdateProfilePersonalForm.php
  • app/Http/Livewire/Profile/UpdateSettingsForm.php

Problem:

  • Component middleware checked permissions on wrong guard
  • Mount methods used hasPermissionTo() instead of can()

Impact: Could not access organization/bank profile edit pages even with proper permissions.

Solution:

  • Commented out component middleware (moved authorization to mount() method)
  • Changed all permission checks from hasPermissionTo() to can()
  • Ensured all checks validate against web user, not active profile

5. Profile Switching Authorization - Cross-Guard Blocking

File: app/Http/Livewire/SwitchProfile.php:193

Problem: Used ProfileAuthorizationHelper::can() which enforces cross-guard protection. During profile switching, user is on 'web' guard trying to access 'admin'/'bank'/'organization' guard profile, so cross-guard check blocked the switch.

Impact: Users could not switch from User profile to Organization/Bank/Admin profiles, seeing "Unauthorized profile switch attempt" errors.

Solution: Created and used ProfileAuthorizationHelper::userOwnsProfile() method specifically for profile switching that checks ownership without cross-guard enforcement.

Files Modified

Core Authorization Infrastructure

  1. app/Http/Middleware/CanOnWebGuard.php (lines 10-21)

    • Changed from hasPermissionTo($permission, 'web') to can($permission)
    • Added explanatory comments about multi-guard system
  2. app/Providers/AuthServiceProvider.php (lines 65-99)

    • Updated Gate definitions for 'manage banks', 'manage organizations', 'manage admins'
    • Added try-catch blocks and instanceof checks
    • Changed from hasPermissionTo() to can()
  3. app/Providers/AppServiceProvider.php (lines 106-130)

    • Completely rewrote @usercan directive
    • Now always checks web user permissions
    • Added exception handling for missing permissions

Profile Management Components

  1. app/Http/Livewire/ProfileOrganization/UpdateProfileOrganizationForm.php

    • Commented out protected $middleware (lines 29-34)
    • Updated mount() authorization to check web user (lines 68-76)
  2. app/Http/Livewire/ProfileBank/UpdateProfileBankForm.php

    • Commented out protected $middleware (lines 28-32)
    • Updated mount() authorization to check web user (lines 67-75)
  3. app/Http/Livewire/ProfileUser/UpdateProfilePersonalForm.php

    • Updated mount() authorization (lines 60-64)
  4. app/Http/Livewire/Profile/UpdateSettingsForm.php

    • Updated mount() authorization for all profile types (lines 55-80)

Profile Switching

  1. app/Http/Livewire/SwitchProfile.php (line 193)
    • Changed from ProfileAuthorizationHelper::can() to ProfileAuthorizationHelper::userOwnsProfile()

Test Suite

  1. tests/Feature/Security/Authorization/ProfileAuthorizationHelperTest.php
    • Updated 5 tests to use userOwnsProfile() for cross-guard ownership checks
    • Clarified that direct Admin→Organization or Organization→Bank switching is not supported
    • Application flow requires going through User (web guard) for all profile switches

Security Test Results

Before Fixes

  • Authorization tests: Multiple failures due to permission errors
  • User experience: Could not switch profiles or edit Organization/Bank profiles

After Fixes

All authorization security tests passing:

✓ 21 LivewireMethodAuthorizationTest - Livewire method-level authorization
✓ 21 ExportProfileDataAuthorizationTest - IDOR prevention and data export authorization
✓ 18 ProfileAuthorizationHelperTest - Multi-guard profile access validation

Total: 60/60 tests passing (100%)

Architecture Clarifications

Permission Storage Model

Critical Understanding: In the multi-guard authentication system, ALL permissions are stored only on the 'web' guard. Organization, Bank, and Admin models do NOT have their own permission records.

// ✓ Correct: Always check web user
$webUser = Auth::guard('web')->user();
$webUser->can('manage organizations');

// ✗ Wrong: Organizations don't have permissions
$organization = Auth::guard('organization')->user();
$organization->can('manage organizations'); // Will fail

Profile Switching Flow

Correct Flow: User (web guard) → Admin/Organization/Bank

Incorrect Flow: Admin → Organization (not supported)

Profile switching requires being authenticated on the 'web' guard first. To switch from Admin to Organization, the user must:

  1. Switch back to User profile (web guard)
  2. Then switch to Organization profile

This is enforced by userOwnsProfile() which only checks the web guard user.

Method Usage Guide

ProfileAuthorizationHelper Methods:

  1. can($profile) - For post-switch authorization

    • Use when: Validating access to profile that user is already switched to
    • Enforces: Guard matching (authenticated guard must match profile type)
    • Example: Livewire component checks in mount()
  2. userOwnsProfile($profile) - For profile switching

    • Use when: Validating user can switch to a profile
    • Enforces: Relationship checking only (no guard matching)
    • Example: SwitchProfile component
  3. authorize($profile) - Convenience wrapper

    • Same as can() but throws exception instead of returning boolean

Security Improvements

  1. Consistent Permission Checking: All permission checks now use can() which works with both database permissions and Gates
  2. Proper Error Handling: All permission checks have try-catch blocks to gracefully handle missing permissions
  3. Clear Separation of Concerns: Profile switching (userOwnsProfile()) vs post-switch authorization (can())
  4. Multi-Guard Awareness: All components now properly check web user permissions regardless of active profile
  5. Documentation: Updated tests to reflect correct application architecture and flows

Remaining Test Issues

Note: The broader security test suite has 83 failing tests, but these are pre-existing failures unrelated to this work:

  • Direct Login Routes (20 failures) - Missing factories and routes
  • Multi-Guard Authentication (2 failures) - Route-related issues
  • Profile Switching (11 failures) - Session-related test issues
  • IDOR tests (7 failures) - Missing factories
  • XSS tests (5 failures) - Missing configurations
  • SQL Injection tests (2 failures) - Missing factories

These failures are in tests that existed before this work and are not related to the permission system fixes.

Testing Recommendations

  1. Manual Testing: Verify profile switching works for all profile types
  2. Permission Testing: Verify all navigation links appear correctly when switched to different profiles
  3. Form Testing: Verify organization/bank profile edit pages load and save correctly
  4. Session Testing: Verify behavior when session expires on profile pages

Deployment Notes

Required Steps

  1. Clear all Laravel caches: php artisan optimize:clear
  2. Verify web server has write access to storage directories
  3. Monitor logs for any permission-related warnings

No Migration Required

These changes only affect code, not database structure.

References

  • Multi-Guard Authentication: references/SECURITY_OVERVIEW.md
  • Livewire Authorization: references/LIVEWIRE_METHOD_AUTHORIZATION_SECURITY.md
  • ProfileAuthorizationHelper: app/Helpers/ProfileAuthorizationHelper.php