diff --git a/composer.json b/composer.json index 13de124..b52a3d9 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "dashboard/unifi", "description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform", - "version": "1.7.0", + "version": "1.7.1", "type": "library", "license": "MIT", "autoload": { diff --git a/src/Http/Controllers/UnifiPagesAccessController.php b/src/Http/Controllers/UnifiPagesAccessController.php index 3674d3f..4ab1dbc 100644 --- a/src/Http/Controllers/UnifiPagesAccessController.php +++ b/src/Http/Controllers/UnifiPagesAccessController.php @@ -40,6 +40,15 @@ class UnifiPagesAccessController extends Controller $grantedUserIds = $grants->flatten(1)->where('grantee_type', 'user')->pluck('grantee_id')->unique(); $users = User::whereIn('id', $grantedUserIds)->orderBy('name')->get(['id', 'name', 'email']); + // Groups: always include super-admin groups (locked-on across all + // pages) plus any group with at least one grant. Other groups are + // added via searchGroups. + $grantedGroupIds = $grants->flatten(1)->where('grantee_type', 'group')->pluck('grantee_id')->unique(); + $groups = Group::where(function ($q) use ($grantedGroupIds) { + $q->where('is_super', true) + ->orWhereIn('id', $grantedGroupIds); + })->orderBy('name')->get(['id', 'name', 'is_super']); + return response()->json([ 'pages' => $pages->map(fn ($p) => [ 'id' => $p->id, @@ -49,7 +58,7 @@ class UnifiPagesAccessController extends Controller 'group_ids' => $grants->get($p->id, collect())->where('grantee_type', 'group')->pluck('grantee_id')->all(), ])->values(), 'users' => $users, - 'groups' => Group::orderBy('name')->get(['id', 'name', 'is_super']), + 'groups' => $groups, ]); } @@ -76,6 +85,27 @@ class UnifiPagesAccessController extends Controller return response()->json(['users' => $users]); } + /** + * Typeahead-style search for groups to add to the access matrix. + * Excludes super-admin groups (they're already in the matrix and + * locked-on across every page). + */ + public function searchGroups(Request $request) + { + $q = trim((string) $request->query('q', '')); + if (strlen($q) < 2) { + return response()->json(['groups' => []]); + } + + $groups = Group::where('name', 'like', '%' . $q . '%') + ->where(function ($w) { $w->where('is_super', false)->orWhereNull('is_super'); }) + ->orderBy('name') + ->limit(20) + ->get(['id', 'name', 'is_super']); + + return response()->json(['groups' => $groups]); + } + public function update(Request $request, NavItem $navItem) { $app = DashboardApp::where('slug', 'unifi')->first(); diff --git a/src/Models/UnifiPageGrant.php b/src/Models/UnifiPageGrant.php index 5e384e2..bd11110 100644 --- a/src/Models/UnifiPageGrant.php +++ b/src/Models/UnifiPageGrant.php @@ -29,18 +29,20 @@ class UnifiPageGrant extends Model } /** - * True iff $user is allowed to access $navItem under this grant model. - * Super-admins always pass. - * If there are NO grants for the page, falls back to "open" (anyone - * who can reach the route can access — same as before grants existed). + * True iff $user is allowed to access $navItem under strict allowlist + * semantics: + * * super-admins (the model-level flag) always pass + * * otherwise the user must be a direct grantee, OR be in a group + * that is a grantee + * + * A page with NO grants saved is only visible to super-admins — + * the admin must explicitly authorize everyone else via the + * Access tab. */ public static function userCanAccess(User $user, NavItem $navItem): bool { if ($user->is_super_admin) return true; - $hasGrants = static::where('nav_item_id', $navItem->id)->exists(); - if (! $hasGrants) return true; - $groupIds = $user->groups()->pluck('groups.id'); return static::where('nav_item_id', $navItem->id) diff --git a/src/UnifiServiceProvider.php b/src/UnifiServiceProvider.php index 62c8edf..4edd241 100644 --- a/src/UnifiServiceProvider.php +++ b/src/UnifiServiceProvider.php @@ -44,7 +44,9 @@ class UnifiServiceProvider extends ServiceProvider if (! $item) return; if (! UnifiPageGrant::userCanAccess($user, $item)) { - abort(403, 'You do not have access to this page.'); + // 404 instead of 403 — don't leak that the page + // exists. The Access tab is the only way in. + abort(404); } } catch (\Throwable) { // unifi_page_grants table may not exist yet on a fresh diff --git a/src/routes/unifi.php b/src/routes/unifi.php index 2419439..0b29cc2 100644 --- a/src/routes/unifi.php +++ b/src/routes/unifi.php @@ -78,6 +78,7 @@ Route::middleware(['web', 'auth', 'app.access:unifi']) Route::middleware('super.admin')->group(function () { Route::get('/settings/pages-access', [UnifiPagesAccessController::class, 'index']) ->name('settings.pages-access.index'); Route::get('/settings/pages-access/users/search', [UnifiPagesAccessController::class, 'searchUsers'])->name('settings.pages-access.users.search'); + Route::get('/settings/pages-access/groups/search', [UnifiPagesAccessController::class, 'searchGroups'])->name('settings.pages-access.groups.search'); Route::put('/settings/pages-access/{navItem}', [UnifiPagesAccessController::class, 'update']) ->name('settings.pages-access.update'); });