mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 03:06:23 +00:00
179 lines
6.8 KiB
PHP
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);
|
|
}
|
|
}
|