diff --git a/composer.json b/composer.json index 8e1f3e0..fb7e942 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ { "key": "unifi.stats", "label": "View Network Stats", "description": "View WiFi dashboards, AP stats, and client lists" }, { "key": "unifi.manage", "label": "Manage Network", "description": "Reboot APs, manage SSIDs, change WiFi passwords" }, { "key": "unifi.auth", "label": "Manage Portal Auth", "description": "Configure captive portal, VLAN mappings, MAC allowlist" }, - { "key": "unifi.settings", "label": "Network Settings", "description": "Configure UniFi controller connection" } + { "key": "unifi.settings", "label": "Network Settings", "description": "Configure UniFi controller connection" }, + { "key": "unifi.guest_wifi", "label": "View Guest Wi-Fi Password", "description": "See the guest network name, its current rotating password, and the join QR code on the home page. Grants no other network access." } ] } }, diff --git a/src/Http/Controllers/GuestCardController.php b/src/Http/Controllers/GuestCardController.php index a13f5d7..d32ef18 100644 --- a/src/Http/Controllers/GuestCardController.php +++ b/src/Http/Controllers/GuestCardController.php @@ -57,29 +57,41 @@ class GuestCardController extends Controller * unchecked, so any signed-in account — every student and parent included — * could fetch the live guest Wi-Fi password by requesting the URL directly. * - * The endpoint now enforces what the comment always claimed: you may poll it iff - * you may see the card. Super admins pass, as everywhere else. Kept as the card - * gate rather than a permission so the widget stays grantable to non-unifi staff, - * which is the point of a home-page card. + * The endpoint enforces what the comment always claimed: you may poll it iff + * you may see the card. Still asked as the CARD gate — via the registry, which + * now answers from the card's permission (unifi.guest_wifi) instead of the + * retired dashboard_card_grants table. Asking the registry rather than naming + * the key here is deliberate: the endpoint and the home page cannot drift. + * Super admins pass, as everywhere else (keysFor handles that). */ public function data(Request $request) { $user = $request->user(); abort_unless($user, 403); - if (! $user->is_super_admin) { - $allowed = \App\Models\DashboardCardGrant::accessibleCardKeysFor($user) - ->contains(self::CARD_KEY); - - abort_unless($allowed, 403); - } + abort_unless( + app(\App\Support\DashboardCardRegistry::class)->visibleTo($user, self::CARD_KEY), + 403, + ); return response()->json(self::payload() ?? ['title' => null]); } - /** Card registry resolver. */ + /** + * Card registry resolver. + * + * Re-checks the permission the registry already gated on, the way bosch's and + * signinout's resolvers do. Redundant on the home-page path and deliberately + * kept: renderFor() renders whatever key list it is handed, and the payload + * here is a live credential — the one card where "the caller asked the right + * question" is not a good enough reason to hand it over. + */ public static function resolve(User $user): ?array { + if (! $user->can('unifi.guest_wifi')) { + return null; + } + return self::payload(); } diff --git a/src/UnifiServiceProvider.php b/src/UnifiServiceProvider.php index 019b82f..f719f4a 100644 --- a/src/UnifiServiceProvider.php +++ b/src/UnifiServiceProvider.php @@ -22,9 +22,22 @@ class UnifiServiceProvider extends ServiceProvider $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); // "/" home card: guest Wi-Fi network + current rotating password + - // QR (re-renders when the password changes). Grant-gated via - // Admin → Settings → Dashboard Cards; self-hides until enabled and - // a password has been rotated. + // QR (re-renders when the password changes). Self-hides until enabled + // and a password has been rotated. + // + // The ONE card in the platform that earned a NEW permission key rather + // than reusing one. It cannot be universal — the payload is a live + // credential, and publishing it to every student and parent is the exact + // hole GuestCardController::data was written to close. And no existing + // unifi key fits: unifi.stats is the network dashboards, unifi.settings is + // the controller connection, and handing the front desk either one to let + // them read a guest password would widen access, not reuse it. So the + // granularity goes in the catalogue (same move as the papercut/inventory + // split), and the key is grantable to non-unifi staff — which is the whole + // point of a home-page card. + // + // Declared in composer.json extra.dashboard.permissions, so the catalogue + // row appears when unifi is installed/updated — NOT seeded to any group. try { app(\App\Support\DashboardCardRegistry::class)->register( 'unifi.guest_wifi', @@ -32,6 +45,7 @@ class UnifiServiceProvider extends ServiceProvider 'UnifiGuestWifi', fn (\App\Models\User $user) => \Dashboard\Unifi\Http\Controllers\GuestCardController::resolve($user), 'The guest network name, current password, and a join QR code — updates automatically when the password rotates.', + permission: 'unifi.guest_wifi', ); } catch (\Throwable) { // Older shell without the card registry — skip. diff --git a/src/routes/unifi.php b/src/routes/unifi.php index 7054c51..63ed482 100644 --- a/src/routes/unifi.php +++ b/src/routes/unifi.php @@ -92,9 +92,11 @@ Route::middleware(['web', 'auth', 'app.access:unifi']) }); }); -// ── Guest Wi-Fi home-card poll (any logged-in user; card visibility is -// grant-gated by the shell). Exempt from the unifi page-grant middleware -// because it's a home-page widget, not a unifi page. ── +// ── Guest Wi-Fi home-card poll. Authorises inside the controller against the +// card's own visibility (unifi.guest_wifi) — it returns a live credential, so +// `auth` alone is not a gate. Deliberately outside the permission groups above +// because it's a home-page widget, not a unifi page: unifi.guest_wifi is +// grantable to front-desk staff without any other network access. ── Route::middleware(['web', 'auth']) ->get('/app/unifi/guest-card/data', [\Dashboard\Unifi\Http\Controllers\GuestCardController::class, 'data']) ->name('unifi.guest-card.data');