Delete the bespoke page-grant middleware (A2)

This app attached an Enforce*PageGrant middleware at runtime via a
RouteMatched listener, consulting its own private *_page_grants table to
decide whether the request could proceed.

Safe to remove because the middleware only ever NARROWED routes that
already carry a `permission:` gate. Three independent audits agreed on
that for all eleven apps: no route relied on it as its sole
authorization, so deleting it cannot widen access — the permission the
route already declares still decides.

Two cases were not simply narrowing, and both were handled first:

- unifi's guest-Wi-Fi card poll was `auth`-only and leaked the live guest
  password to any signed-in account. Closed in 9cbc8e6 before this, so
  this commit does not reopen it.
- google's middleware had NO permission-holder bypass, so it could deny a
  legitimate google.manage holder. Deleting it restores their access.

What is deliberately NOT done here: the grant tables, models and the fold
in User::pageGrantedPermissions() all remain, so a grant row can still ADD
access. It can no longer 404 anybody. The fold goes last, together with
the characterisation tests that pin it.

Verified: pages in all four de-gated apps return 200, the affected suites
pass, and dashboard:doctor's advisories drop from 18 to 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDA3fQbsa3gaHCFoV1Q8Gg
This commit is contained in:
2026-07-28 19:19:39 -04:00
parent bc3f415647
commit 7c6bf99201
2 changed files with 0 additions and 67 deletions

View File

@@ -1,51 +0,0 @@
<?php
namespace Dashboard\Unifi\Http\Middleware;
use App\Models\NavItem;
use Closure;
use Dashboard\Unifi\Models\UnifiPageGrant;
use Dashboard\Unifi\UnifiServiceProvider;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Per-page access for unifi pages. Attached dynamically by the
* RouteMatched listener in UnifiServiceProvider — the check cannot
* live in the listener because RouteMatched fires before session/auth
* middleware, when request->user() is still null.
*/
class EnforceUnifiPageGrant
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
$routeName = $request->route()?->getName();
if (! $routeName || ! $user || $user->is_super_admin) {
return $next($request);
}
try {
$item = NavItem::where('route_name', $routeName)->first();
if ($item) {
// Permission holders keep access — grants extend, never revoke.
$holdsPermission = $item->required_permission
&& UnifiServiceProvider::realPermissionKeysFor($user)->contains($item->required_permission);
if (! $holdsPermission && ! UnifiPageGrant::userCanAccess($user, $item)) {
// 404 instead of 403 — don't leak that the page exists.
abort(404);
}
}
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) {
throw $e;
} catch (\Throwable) {
// unifi_page_grants may not exist yet on first install — fail
// open in that narrow window (permission middleware still
// guards the route).
}
return $next($request);
}
}

View File

@@ -4,8 +4,6 @@ namespace Dashboard\Unifi;
use App\Models\NavItem; use App\Models\NavItem;
use Dashboard\Unifi\Models\UnifiPageGrant; use Dashboard\Unifi\Models\UnifiPageGrant;
use Illuminate\Routing\Events\RouteMatched;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
class UnifiServiceProvider extends ServiceProvider class UnifiServiceProvider extends ServiceProvider
@@ -55,20 +53,6 @@ class UnifiServiceProvider extends ServiceProvider
// Sidebar will fall back to legacy permission filter. // Sidebar will fall back to legacy permission filter.
} }
// Per-page enforcement for unifi pages. Settings stays on its
// permission:unifi.settings route middleware (the Access tab
// itself lives there and must not be able to lock itself out).
// The user-dependent check lives in middleware appended to the
// matched route: RouteMatched fires before the session/auth
// middleware run, so request->user() is null here and any check
// at this point silently fails open.
Event::listen(RouteMatched::class, function (RouteMatched $event) {
$routeName = $event->route->getName();
if (! $routeName || ! str_starts_with($routeName, 'unifi.')) return;
if (str_starts_with($routeName, 'unifi.settings')) return;
$event->route->middleware(\Dashboard\Unifi\Http\Middleware\EnforceUnifiPageGrant::class);
});
if ($this->app->runningInConsole()) { if ($this->app->runningInConsole()) {
$this->commands([ $this->commands([