server->set('REMOTE_ADDR', $ip); } beforeEach(function () { config()->set('admin_access.hosts', []); config()->set('admin_access.trusted_ranges', ['10.66.0.0/24']); Settings::set('console.network_restricted', false); Settings::set('console.allowed_ips', []); }); it('changes nothing until the owner switches it on', function () { $this->actingAs(operator('Owner')) ->withServerVariables(['REMOTE_ADDR' => '203.0.113.9']) ->get('/admin') ->assertOk(); }); it('lets the management VPN in and turns everyone else away', function () { Settings::set('console.network_restricted', true); $this->actingAs(operator('Owner')) ->withServerVariables(['REMOTE_ADDR' => '10.66.0.5']) ->get('/admin') ->assertOk(); // 404, never 403: a stranger must not learn a console lives here. $this->actingAs(operator('Owner')) ->withServerVariables(['REMOTE_ADDR' => '203.0.113.9']) ->get('/admin') ->assertNotFound(); }); it('lets in an address the owner has listed, without the VPN', function () { Settings::set('console.network_restricted', true); Settings::set('console.allowed_ips', ['203.0.113.0/24']); $this->actingAs(operator('Owner')) ->withServerVariables(['REMOTE_ADDR' => '203.0.113.9']) ->get('/admin') ->assertOk(); // A neighbouring range is still nobody. $this->actingAs(operator('Owner')) ->withServerVariables(['REMOTE_ADDR' => '198.51.100.4']) ->get('/admin') ->assertNotFound(); }); it('knows when a change would lock the owner out', function () { // The one check whose failure mode is that nobody can reach the page that // would fix it — so it is decided without a request, and tested that way. expect(RestrictConsoleNetwork::wouldStillAllow('10.66.0.5', []))->toBeTrue() ->and(RestrictConsoleNetwork::wouldStillAllow('198.51.100.4', []))->toBeFalse() ->and(RestrictConsoleNetwork::wouldStillAllow('198.51.100.4', ['198.51.100.0/24']))->toBeTrue() // Removing the entry you are sitting behind leaves you outside. ->and(RestrictConsoleNetwork::wouldStillAllow('203.0.113.9', []))->toBeFalse(); }); it('rejects an entry that is not an address or a range', function () { // A typo that silently matches nothing is how someone locks themselves out // while believing they have not. foreach (['nonsense', '203.0.113.', '203.0.113.9/99', ''] as $bad) { Livewire::actingAs(operator('Owner')) ->test(AdminSettings::class) ->set('consoleIp', $bad) ->call('addConsoleIp') ->assertHasErrors('consoleIp'); } expect(Settings::get('console.allowed_ips'))->toBe([]); Livewire::actingAs(operator('Owner')) ->test(AdminSettings::class) ->set('consoleIp', '203.0.113.0/24') ->call('addConsoleIp') ->assertHasNoErrors(); expect(Settings::get('console.allowed_ips'))->toBe(['203.0.113.0/24']); }); it('needs the capability, not merely an operator account', function () { Livewire::actingAs(operator('Support')) ->test(AdminSettings::class) ->call('addConsoleIp') ->assertForbidden(); }); it('is re-checked on every Livewire action, not only on the page', function () { // The page is not where the actions run. $persistent = app(\Livewire\Mechanisms\PersistentMiddleware\PersistentMiddleware::class) ->getPersistentMiddleware(); expect($persistent)->toContain(RestrictConsoleNetwork::class); });