3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 03:06:23 +00:00
Files
snipe-it/tests/Feature/Fmcs/HttpCrossCompanyLeakTest.php
2026-07-07 10:01:42 +01:00

179 lines
6.8 KiB
PHP

<?php
namespace Tests\Feature\Fmcs;
use App\Models\Accessory;
use App\Models\Asset;
use App\Models\Company;
use App\Models\Component;
use App\Models\Consumable;
use App\Models\Department;
use App\Models\License;
use App\Models\Location;
use App\Models\User;
use Illuminate\Database\Eloquent\Model;
use Tests\TestCase;
/**
* End-to-end tenant isolation guard.
*
* Every companyable list endpoint is exercised as a NON-SUPERUSER caller
* scoped to company A, with a matching row present in company B. If any
* row belonging to company B appears in the response, the test fails and
* we know a scoping regression is loose in HTTP land, even if the
* model-level scope tests are still green (e.g. a controller called
* withoutGlobalScopes, joined bypassed a scope, or used a raw query).
*
* Users are covered by tests/Feature/Users/Api/IndexUsersTest;
* tests/Unit/CompanyScopingTest covers the model-level equivalents. This
* suite is the outer defense — controller-level and route-level.
*/
class HttpCrossCompanyLeakTest extends TestCase
{
protected Company $companyA;
protected Company $companyB;
protected function setUp(): void
{
parent::setUp();
[$this->companyA, $this->companyB] = Company::factory()->count(2)->create();
}
/**
* Grant a non-superuser caller "view all" permissions for the given
* resource keys and pin them to company A. Superusers bypass FMCS
* entirely so we can't use them here; a plain view permission plus
* company_user pivot to company A is the shape a real customer's
* non-admin employee has.
*/
private function callerScopedToCompanyA(array $viewPermissionKeys): User
{
$permissions = [];
foreach ($viewPermissionKeys as $key) {
$permissions[$key.'.view'] = '1';
}
return $this->companyA->users()->save(
User::factory()->make(['permissions' => json_encode($permissions)])
);
}
private function idsFromResponse(array $rows): array
{
return collect($rows)->pluck('id')->all();
}
private function assertCompanyBRowHidden(string $endpoint, Model $companyBRow, User $caller, string $rowsKey = 'rows'): void
{
$ids = $this->idsFromResponse(
$this->actingAsForApi($caller)
->getJson($endpoint)
->assertOk()
->json($rowsKey)
);
$this->assertNotContains(
$companyBRow->id,
$ids,
'Cross-company leak at '.$endpoint.': company B '.get_class($companyBRow).' id '.$companyBRow->id.' visible to a company A caller.',
);
}
public function test_api_assets_index_hides_other_company_rows(): void
{
$companyBAsset = Asset::factory()->for($this->companyB)->create();
$caller = $this->callerScopedToCompanyA(['assets']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.assets.index'), $companyBAsset, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.assets.index'), $companyBAsset, $caller);
}
public function test_api_accessories_index_hides_other_company_rows(): void
{
$companyBAccessory = Accessory::factory()->for($this->companyB)->create();
$caller = $this->callerScopedToCompanyA(['accessories']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.accessories.index'), $companyBAccessory, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.accessories.index'), $companyBAccessory, $caller);
}
public function test_api_consumables_index_hides_other_company_rows(): void
{
$companyBConsumable = Consumable::factory()->for($this->companyB)->create();
$caller = $this->callerScopedToCompanyA(['consumables']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.consumables.index'), $companyBConsumable, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.consumables.index'), $companyBConsumable, $caller);
}
public function test_api_components_index_hides_other_company_rows(): void
{
$companyBComponent = Component::factory()->for($this->companyB)->create();
$caller = $this->callerScopedToCompanyA(['components']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.components.index'), $companyBComponent, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.components.index'), $companyBComponent, $caller);
}
public function test_api_licenses_index_hides_other_company_rows(): void
{
$companyBLicense = License::factory()->for($this->companyB)->create();
$caller = $this->callerScopedToCompanyA(['licenses']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.licenses.index'), $companyBLicense, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.licenses.index'), $companyBLicense, $caller);
}
public function test_api_locations_index_hides_other_company_rows(): void
{
$companyBLocation = Location::factory()->for($this->companyB)->create();
$caller = $this->callerScopedToCompanyA(['locations']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.locations.index'), $companyBLocation, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.locations.index'), $companyBLocation, $caller);
}
public function test_api_departments_index_hides_other_company_rows(): void
{
$companyBDepartment = Department::factory()->for($this->companyB)->create();
$caller = $this->callerScopedToCompanyA(['departments']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.departments.index'), $companyBDepartment, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.departments.index'), $companyBDepartment, $caller);
}
public function test_api_users_index_hides_other_company_users(): void
{
$companyBUser = $this->companyB->users()->save(User::factory()->make());
$caller = $this->callerScopedToCompanyA(['users']);
$this->settings->enableMultipleFullCompanySupport();
$this->assertCompanyBRowHidden(route('api.users.index'), $companyBUser, $caller);
$this->settings->enableFloaterMode();
$this->assertCompanyBRowHidden(route('api.users.index'), $companyBUser, $caller);
}
}