Initial commit
This commit is contained in:
160
app/Http/Livewire/Traits/RequiresAdminAuthorization.php
Normal file
160
app/Http/Livewire/Traits/RequiresAdminAuthorization.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\Traits;
|
||||
|
||||
use App\Helpers\ProfileAuthorizationHelper;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Requires Admin Authorization Trait
|
||||
*
|
||||
* Provides reusable admin authorization checks for Livewire components.
|
||||
* Prevents IDOR attacks and cross-guard access by validating every sensitive
|
||||
* operation, not just the initial component mount.
|
||||
*
|
||||
* CRITICAL: mount() protection alone is insufficient because Livewire methods
|
||||
* can be called directly after the component loads, bypassing mount() checks.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Add trait to component: use RequiresAdminAuthorization;
|
||||
* 2. Call at start of sensitive methods: $this->authorizeAdminAccess();
|
||||
*
|
||||
* Example:
|
||||
* public function deletePost($id)
|
||||
* {
|
||||
* $this->authorizeAdminAccess(); // CRITICAL: Check on every call
|
||||
*
|
||||
* // Safe to proceed with deletion
|
||||
* Post::destroy($id);
|
||||
* }
|
||||
*/
|
||||
trait RequiresAdminAuthorization
|
||||
{
|
||||
/**
|
||||
* Cached authorization result to avoid repeated checks within same request
|
||||
*
|
||||
* @var bool|null
|
||||
*/
|
||||
private $adminAuthorizationChecked = null;
|
||||
|
||||
/**
|
||||
* Authorize admin access for the current operation.
|
||||
*
|
||||
* This method MUST be called at the start of every sensitive operation
|
||||
* (create, update, delete, etc.) to prevent unauthorized access.
|
||||
*
|
||||
* Validates:
|
||||
* - Active profile exists in session
|
||||
* - Profile ownership via ProfileAuthorizationHelper (prevents cross-guard attacks)
|
||||
* - Admin or central bank (level=0) permissions
|
||||
*
|
||||
* @param bool $forceRecheck Force a new authorization check even if already checked
|
||||
* @return void
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException (403 or 401)
|
||||
*/
|
||||
protected function authorizeAdminAccess(bool $forceRecheck = false): void
|
||||
{
|
||||
// Use cached result unless force recheck
|
||||
if (!$forceRecheck && $this->adminAuthorizationChecked === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get active profile from session
|
||||
$activeProfileType = session('activeProfileType');
|
||||
$activeProfileId = session('activeProfileId');
|
||||
|
||||
if (!$activeProfileType || !$activeProfileId) {
|
||||
Log::warning('RequiresAdminAuthorization: No active profile in session', [
|
||||
'component' => static::class,
|
||||
'method' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? 'unknown',
|
||||
'ip_address' => request()->ip(),
|
||||
]);
|
||||
|
||||
abort(403, __('No active profile selected'));
|
||||
}
|
||||
|
||||
$profile = $activeProfileType::find($activeProfileId);
|
||||
|
||||
if (!$profile) {
|
||||
Log::warning('RequiresAdminAuthorization: Active profile not found', [
|
||||
'component' => static::class,
|
||||
'active_profile_type' => $activeProfileType,
|
||||
'active_profile_id' => $activeProfileId,
|
||||
'method' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? 'unknown',
|
||||
'ip_address' => request()->ip(),
|
||||
]);
|
||||
|
||||
abort(403, __('Active profile not found'));
|
||||
}
|
||||
|
||||
// Validate profile ownership using ProfileAuthorizationHelper
|
||||
// This prevents cross-guard attacks (e.g., web user accessing admin profile)
|
||||
ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
// Verify admin or central bank permissions
|
||||
if ($profile instanceof \App\Models\Admin) {
|
||||
// Admin access OK
|
||||
$this->adminAuthorizationChecked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if ($profile instanceof \App\Models\Bank) {
|
||||
// Only central bank (level 0) can access admin functions
|
||||
if ($profile->level === 0) {
|
||||
$this->adminAuthorizationChecked = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Log::warning('RequiresAdminAuthorization: Non-central bank attempted admin operation', [
|
||||
'component' => static::class,
|
||||
'method' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? 'unknown',
|
||||
'bank_id' => $profile->id,
|
||||
'bank_level' => $profile->level,
|
||||
'ip_address' => request()->ip(),
|
||||
]);
|
||||
|
||||
abort(403, __('Central bank access required for admin operations'));
|
||||
}
|
||||
|
||||
// Not admin or central bank
|
||||
Log::warning('RequiresAdminAuthorization: Unauthorized profile type attempted admin operation', [
|
||||
'component' => static::class,
|
||||
'method' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? 'unknown',
|
||||
'profile_type' => get_class($profile),
|
||||
'profile_id' => $profile->id,
|
||||
'ip_address' => request()->ip(),
|
||||
]);
|
||||
|
||||
abort(403, __('Admin or central bank access required'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset authorization cache.
|
||||
*
|
||||
* Call this if you need to force a fresh authorization check
|
||||
* (e.g., after switching profiles within the same component lifecycle).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function resetAdminAuthorization(): void
|
||||
{
|
||||
$this->adminAuthorizationChecked = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if admin access is authorized without throwing an exception.
|
||||
*
|
||||
* Useful for conditional UI rendering or feature availability checks.
|
||||
*
|
||||
* @return bool True if authorized, false otherwise
|
||||
*/
|
||||
protected function canPerformAdminActions(): bool
|
||||
{
|
||||
try {
|
||||
$this->authorizeAdminAccess();
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user