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); } }