Send a guest bounced off the console to the console's own sign-in
ApplicationBuilder::withMiddleware() defaults redirectGuestsTo() to
fn () => route('login') before bootstrap/app.php's own callback ever
runs — always the portal's Fortify page, regardless of which side of
the site turned the guest away. In non-exclusive (shared, this VM's
own mode) that sends a guest bounced off /admin to a sign-in form whose
table holds no operators: no 404 any more, but a sign-in that can never
succeed. Exclusive mode happened to self-correct only because
RestrictAdminHost 404s the wrong host before this ever ran.
Fixed for both modes at once rather than depending on which one a given
deployment runs: ask AdminArea, the one place routes/web.php's own
registration and RestrictAdminHost already agree is the source of truth
for "is this the console".
feat/operator-identity
parent
47812cfca9
commit
5e22e16291
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Support\AdminArea;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
|
@ -52,6 +53,22 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||
|
||||
// Stripe posts server-to-server with its own signature (no CSRF token).
|
||||
$middleware->validateCsrfTokens(except: ['webhooks/stripe']);
|
||||
|
||||
// ApplicationBuilder::withMiddleware() defaults this to
|
||||
// fn () => route('login') BEFORE this callback ever runs — always the
|
||||
// PORTAL's Fortify page, regardless of which side of the site the
|
||||
// guest was actually turned away from. In non-exclusive (shared)
|
||||
// mode that sends a guest bounced off /admin to a sign-in form whose
|
||||
// table holds no operators; exclusive mode happened to self-correct
|
||||
// only because RestrictAdminHost 404s the wrong host before this
|
||||
// ever ran. Fixed for both modes at once, rather than relying on
|
||||
// which one a given deployment happens to run: ask AdminArea, which
|
||||
// both routes/web.php's own registration and RestrictAdminHost
|
||||
// already treat as the one source of truth for "is this the
|
||||
// console".
|
||||
$middleware->redirectGuestsTo(
|
||||
fn (Request $request) => AdminArea::isConsole($request) ? route('admin.login') : route('login'),
|
||||
);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
$exceptions->shouldRenderJsonWhen(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,44 @@ use App\Models\User;
|
|||
|
||||
$adminRoutes = ['admin.overview', 'admin.customers', 'admin.instances', 'admin.hosts', 'admin.provisioning', 'admin.revenue'];
|
||||
|
||||
it('redirects guests from the admin console to login', function (string $route) {
|
||||
$this->get(route($route))->assertRedirect('/login');
|
||||
it('redirects guests from the admin console to the CONSOLE login, not the portal one', function (string $route) {
|
||||
// Not '/login': that is Fortify's portal page, whose table holds no
|
||||
// operators — a sign-in a guest bounced off the console could never
|
||||
// complete. bootstrap/app.php's redirectGuestsTo() asks AdminArea which
|
||||
// side this request was for. This suite's default (ADMIN_HOSTS="",
|
||||
// phpunit.xml) is non-exclusive/shared mode; the exclusive-mode side of
|
||||
// the same fix is covered separately below.
|
||||
$this->get(route($route))->assertRedirect(route('admin.login'));
|
||||
})->with($adminRoutes);
|
||||
|
||||
it('sends a guest to the right login for whichever side they hit, in exclusive mode too', function () {
|
||||
// The other of the two modes redirectGuestsTo() has to get right — see
|
||||
// the shared-mode version of this test above. AdminArea::isConsole()
|
||||
// reads admin_access.* LIVE, per request (unlike route domain-binding,
|
||||
// which this suite's routes do not carry: they are compiled once at
|
||||
// boot under the default non-exclusive ADMIN_HOSTS="", so both
|
||||
// route('admin.login') and route('login') below resolve against
|
||||
// whichever host the current request came in on, exactly as production
|
||||
// would resolve them for a request truly received on that host).
|
||||
config()->set('admin_access.hosts', ['admin.example.test']);
|
||||
config()->set('admin_access.exclusive', true);
|
||||
|
||||
$consoleLocation = $this->get('http://admin.example.test/admin')
|
||||
->assertRedirect()->headers->get('Location');
|
||||
|
||||
expect(parse_url($consoleLocation, PHP_URL_HOST))->toBe('admin.example.test')
|
||||
->and(parse_url($consoleLocation, PHP_URL_PATH))->toBe('/admin/login');
|
||||
|
||||
// A different, non-console host in the same exclusive setup still gets
|
||||
// the PORTAL's sign-in page — this fix is not a blanket change to every
|
||||
// guest redirect, only to the ones landing on the console side.
|
||||
$portalLocation = $this->get('http://app.example.test/dashboard')
|
||||
->assertRedirect()->headers->get('Location');
|
||||
|
||||
expect(parse_url($portalLocation, PHP_URL_HOST))->toBe('app.example.test')
|
||||
->and(parse_url($portalLocation, PHP_URL_PATH))->toBe('/login');
|
||||
});
|
||||
|
||||
it('forbids non-admin users from the admin console', function (string $route) {
|
||||
$user = User::factory()->create();
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use Livewire\Livewire;
|
|||
// admin() helper is defined in HostManagementTest (global Pest function).
|
||||
|
||||
it('gates the datacenters page', function () {
|
||||
$this->get(route('admin.datacenters'))->assertRedirect('/login');
|
||||
$this->get(route('admin.datacenters'))->assertRedirect(route('admin.login'));
|
||||
$this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.datacenters'))->assertForbidden();
|
||||
$this->actingAs(admin(), 'operator')->get(route('admin.datacenters'))->assertOk()->assertSee(__('datacenters.title'));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ use Illuminate\Support\Facades\Queue;
|
|||
use Livewire\Livewire;
|
||||
|
||||
it('gates the add-host page', function () {
|
||||
$this->get(route('admin.hosts.create'))->assertRedirect('/login');
|
||||
$this->get(route('admin.hosts.create'))->assertRedirect(route('admin.login'));
|
||||
$this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.hosts.create'))->assertForbidden();
|
||||
$this->actingAs(admin(), 'operator')->get(route('admin.hosts.create'))->assertOk()->assertSee(__('hosts.create_title'));
|
||||
});
|
||||
|
||||
it('gates the host detail page', function () {
|
||||
$host = Host::factory()->create();
|
||||
$this->get(route('admin.hosts.show', $host))->assertRedirect('/login');
|
||||
$this->get(route('admin.hosts.show', $host))->assertRedirect(route('admin.login'));
|
||||
$this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.hosts.show', $host))->assertForbidden();
|
||||
$this->actingAs(admin(), 'operator')->get(route('admin.hosts.show', $host))->assertOk()->assertSee($host->name);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ it('shows real provisioning runs to admins', function () {
|
|||
});
|
||||
|
||||
it('gates the provisioning console to admins', function () {
|
||||
$this->get(route('admin.provisioning'))->assertRedirect('/login');
|
||||
$this->get(route('admin.provisioning'))->assertRedirect(route('admin.login'));
|
||||
$this->actingAs(User::factory()->create())
|
||||
->get(route('admin.provisioning'))->assertForbidden();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue