Gate the guest Wi-Fi card on its own key, and gate its poll endpoint properly
The home card used to be grant-gated by the shell's card table, which left the poll endpoint behind `auth` alone — and that endpoint returns a LIVE CREDENTIAL: the guest network's current rotating password and its join QR code. "Any logged-in user" was never an adequate gate for that; the card table was doing the work, and the card table is gone. New key rather than reuse: unifi.guest_wifi exists precisely so front-desk staff can be given the guest password WITHOUT any other network access. Reusing unifi.settings would have meant handing out controller configuration to get a Wi-Fi password read. Authorised inside the controller rather than by route middleware, so the endpoint stays outside the unifi page-permission groups — it is a home-page widget, not a unifi page. Nobody holds the new key until it is granted, so the card is invisible until then. That is the intended direction: capability is assigned deliberately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDA3fQbsa3gaHCFoV1Q8Gg
This commit is contained in:
@@ -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." }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user