92 lines
2.7 KiB
PHP
92 lines
2.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Support\ApiError;
|
|
use Closure;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class TenantIsolation
|
|
{
|
|
/**
|
|
* Handle an incoming request.
|
|
*/
|
|
public function handle(Request $request, Closure $next)
|
|
{
|
|
$tenantId = $request->attributes->get('tenant_id');
|
|
|
|
if (! $tenantId) {
|
|
return $this->missingTenantIdResponse();
|
|
}
|
|
|
|
// Get the tenant from request (query param, route param, or header)
|
|
$requestTenantId = $this->getTenantIdFromRequest($request);
|
|
|
|
if ($requestTenantId && $requestTenantId != $tenantId) {
|
|
return $this->tenantIsolationViolationResponse((int) $tenantId, (int) $requestTenantId);
|
|
}
|
|
|
|
// Set tenant context for query scoping
|
|
$connection = DB::connection();
|
|
if (in_array($connection->getDriverName(), ['mysql', 'mariadb'])) {
|
|
$connection->statement('SET @tenant_id = ?', [$tenantId]);
|
|
}
|
|
|
|
// Add tenant context to request for easy access in controllers
|
|
$request->attributes->set('current_tenant_id', $tenantId);
|
|
|
|
return $next($request);
|
|
}
|
|
|
|
/**
|
|
* Extract tenant ID from request
|
|
*/
|
|
private function getTenantIdFromRequest(Request $request): ?int
|
|
{
|
|
// 1. Route parameter (e.g., /api/v1/tenant/123/events)
|
|
if ($request->route('tenant')) {
|
|
return (int) $request->route('tenant');
|
|
}
|
|
|
|
// 2. Query parameter (e.g., ?tenant_id=123)
|
|
if ($request->query('tenant_id')) {
|
|
return (int) $request->query('tenant_id');
|
|
}
|
|
|
|
// 3. Header (X-Tenant-ID)
|
|
if ($request->header('X-Tenant-ID')) {
|
|
return (int) $request->header('X-Tenant-ID');
|
|
}
|
|
|
|
// 4. For tenant-specific resources, use token tenant_id
|
|
return null;
|
|
}
|
|
|
|
private function missingTenantIdResponse(): JsonResponse
|
|
{
|
|
return ApiError::response(
|
|
'tenant_context_missing',
|
|
'Tenant Context Missing',
|
|
'Tenant ID not found in access token.',
|
|
Response::HTTP_UNAUTHORIZED
|
|
);
|
|
}
|
|
|
|
private function tenantIsolationViolationResponse(int $tokenTenantId, int $requestTenantId): JsonResponse
|
|
{
|
|
return ApiError::response(
|
|
'tenant_isolation_violation',
|
|
'Tenant Isolation Violation',
|
|
'The requested resource belongs to a different tenant.',
|
|
Response::HTTP_FORBIDDEN,
|
|
[
|
|
'token_tenant_id' => $tokenTenantId,
|
|
'request_tenant_id' => $requestTenantId,
|
|
]
|
|
);
|
|
}
|
|
}
|