user() ?: Auth::guard('bank')->user() ?: Auth::guard('organization')->user() ?: Auth::guard('web')->user(); } /** * Validate that the authenticated user has ownership/access to a profile. * * This function prevents IDOR attacks by ensuring: * - Users can only access their own User profile * - Users can only access Organizations they're linked to * - Users can only access Banks they're linked to * - Users can only access Admin profiles they're linked to * * @param mixed $profile The profile to validate (User, Organization, Bank, or Admin) * @param bool $throwException Whether to throw 403 exception (default: true) * @return bool True if authorized, false if not (when $throwException = false) * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public static function validateProfileOwnership($profile, bool $throwException = true): bool { $authenticatedProfile = self::getAuthenticatedProfile(); // Must be authenticated if (!$authenticatedProfile) { Log::warning('ProfileAuthorizationHelper: Attempted profile access without authentication', [ 'profile_id' => $profile?->id, 'profile_type' => $profile ? get_class($profile) : null, ]); if ($throwException) { abort(401, 'Authentication required'); } return false; } // IMPORTANT: Verify guard matches profile type to prevent cross-guard attacks // Even if the user has a relationship with the profile, they must be authenticated on the correct guard $expectedGuardForProfile = null; if ($profile instanceof \App\Models\Bank) { $expectedGuardForProfile = 'bank'; } elseif ($profile instanceof \App\Models\Organization) { $expectedGuardForProfile = 'organization'; } elseif ($profile instanceof \App\Models\Admin) { $expectedGuardForProfile = 'admin'; } elseif ($profile instanceof \App\Models\User) { $expectedGuardForProfile = 'web'; } // Check which guard the current authentication is from $currentGuard = null; if (Auth::guard('admin')->check() && Auth::guard('admin')->user() === $authenticatedProfile) { $currentGuard = 'admin'; } elseif (Auth::guard('bank')->check() && Auth::guard('bank')->user() === $authenticatedProfile) { $currentGuard = 'bank'; } elseif (Auth::guard('organization')->check() && Auth::guard('organization')->user() === $authenticatedProfile) { $currentGuard = 'organization'; } elseif (Auth::guard('web')->check() && Auth::guard('web')->user() === $authenticatedProfile) { $currentGuard = 'web'; } // Prevent cross-guard access if ($expectedGuardForProfile && $currentGuard && $expectedGuardForProfile !== $currentGuard) { Log::warning('ProfileAuthorizationHelper: Cross-guard access attempt blocked', [ 'authenticated_guard' => $currentGuard, 'target_profile_type' => get_class($profile), 'expected_guard' => $expectedGuardForProfile, 'profile_id' => $profile->id, ]); if ($throwException) { abort(403, 'Unauthorized: Cannot access ' . class_basename($profile) . ' profile from ' . $currentGuard . ' guard'); } return false; } // Check if authenticated profile is same type and ID as target profile (direct match) if (get_class($authenticatedProfile) === get_class($profile) && $authenticatedProfile->id === $profile->id) { // User is accessing their own profile of same type Log::info('ProfileAuthorizationHelper: Direct profile access authorized', [ 'profile_type' => get_class($profile), 'profile_id' => $profile->id, ]); return true; } // For cross-profile access, we need to check relationships via User model // Get the underlying User for relationship checks $authenticatedUser = null; if ($authenticatedProfile instanceof \App\Models\User) { $authenticatedUser = $authenticatedProfile; } elseif ($authenticatedProfile instanceof \App\Models\Admin) { // Admin can access if they ARE the target admin (already checked above) // For other profile types, need to get linked users if (!($profile instanceof \App\Models\Admin)) { // Admin trying to access non-admin profile - get one of the linked users $authenticatedUser = $authenticatedProfile->users()->first(); } } elseif ($authenticatedProfile instanceof \App\Models\Organization) { // Organization can access if they ARE the target org (already checked above) // For other profile types, need to get linked users if (!($profile instanceof \App\Models\Organization)) { $authenticatedUser = $authenticatedProfile->users()->first(); } } elseif ($authenticatedProfile instanceof \App\Models\Bank) { // Bank can access if they ARE the target bank (already checked above) // For other profile types, need to get linked users if (!($profile instanceof \App\Models\Bank)) { $authenticatedUser = $authenticatedProfile->users()->first(); } } // If we couldn't get a User for relationship checking and it's not a direct match, deny if (!$authenticatedUser) { if ($throwException) { abort(403, 'Unauthorized: Cannot validate cross-profile access'); } return false; } // Validate based on target profile type if ($profile instanceof \App\Models\User) { // User can only access their own user profile if ($profile->id !== $authenticatedUser->id) { Log::warning('ProfileAuthorizationHelper: Unauthorized User profile access attempt', [ 'authenticated_user_id' => $authenticatedUser->id, 'target_user_id' => $profile->id, ]); if ($throwException) { abort(403, 'Unauthorized: You cannot access another user\'s profile'); } return false; } } elseif ($profile instanceof \App\Models\Organization) { // User must be linked to this organization if (!$authenticatedUser->organizations()->where('organization_user.organization_id', $profile->id)->exists()) { Log::warning('ProfileAuthorizationHelper: Unauthorized Organization access attempt', [ 'authenticated_user_id' => $authenticatedUser->id, 'target_organization_id' => $profile->id, 'user_organizations' => $authenticatedUser->organizations()->pluck('organizations.id')->toArray(), ]); if ($throwException) { abort(403, 'Unauthorized: You are not linked to this organization'); } return false; } } elseif ($profile instanceof \App\Models\Bank) { // User must be linked to this bank if (!$authenticatedUser->banksManaged()->where('bank_user.bank_id', $profile->id)->exists()) { Log::warning('ProfileAuthorizationHelper: Unauthorized Bank access attempt', [ 'authenticated_user_id' => $authenticatedUser->id, 'target_bank_id' => $profile->id, 'user_banks' => $authenticatedUser->banksManaged()->pluck('banks.id')->toArray(), ]); if ($throwException) { abort(403, 'Unauthorized: You are not linked to this bank'); } return false; } } elseif ($profile instanceof \App\Models\Admin) { // User must be linked to this admin profile if (!$authenticatedUser->admins()->where('admin_user.admin_id', $profile->id)->exists()) { Log::warning('ProfileAuthorizationHelper: Unauthorized Admin access attempt', [ 'authenticated_user_id' => $authenticatedUser->id, 'target_admin_id' => $profile->id, 'user_admins' => $authenticatedUser->admins()->pluck('admins.id')->toArray(), ]); if ($throwException) { abort(403, 'Unauthorized: You are not linked to this admin profile'); } return false; } } else { // Unknown profile type Log::error('ProfileAuthorizationHelper: Unknown profile type', [ 'profile_type' => get_class($profile), 'profile_id' => $profile?->id, ]); if ($throwException) { abort(500, 'Unknown profile type'); } return false; } // Authorization successful Log::info('ProfileAuthorizationHelper: Profile access authorized', [ 'authenticated_user_id' => $authenticatedUser->id, 'profile_type' => get_class($profile), 'profile_id' => $profile->id, ]); return true; } /** * Validate profile ownership and throw exception if unauthorized. * * Convenience method for the most common use case. * * @param mixed $profile The profile to validate * @return void * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public static function authorize($profile): void { self::validateProfileOwnership($profile, true); } /** * Check if user has access to profile without throwing exception. * * @param mixed $profile The profile to check * @return bool True if authorized, false otherwise */ public static function can($profile): bool { return self::validateProfileOwnership($profile, false); } /** * Check if the web-authenticated user owns a profile (for profile switching). * * This method is specifically for profile switching and does NOT enforce guard matching * since during a switch, the user is authenticated on 'web' guard but wants to access * an elevated profile (Admin, Bank, Organization). * * @param mixed $profile The profile to check ownership of * @return bool True if the web-authenticated user owns this profile */ public static function userOwnsProfile($profile): bool { $user = Auth::guard('web')->user(); if (!$user || !$profile) { return false; } // Check based on profile type if ($profile instanceof \App\Models\User) { return $profile->id === $user->id; } elseif ($profile instanceof \App\Models\Organization) { return $user->organizations()->where('organization_user.organization_id', $profile->id)->exists(); } elseif ($profile instanceof \App\Models\Bank) { return $user->banksManaged()->where('bank_user.bank_id', $profile->id)->exists(); } elseif ($profile instanceof \App\Models\Admin) { return $user->admins()->where('admin_user.admin_id', $profile->id)->exists(); } return false; } }