From f43d11f3e306aed2cf4c73180321b9cb230a41a8 Mon Sep 17 00:00:00 2001 From: nfebe Date: Tue, 22 Sep 2026 21:49:02 +0100 Subject: [PATCH] fix: Close the console reads to anyone who asks The user directory, a single user and the engagement report ran behind authentication alone. Any account with a token could read every user on the deployment. Every other endpoint here already answered to the configured owner. --- CHANGELOG.md | 3 +++ composer.json | 2 +- src/Http/Controllers/AdminController.php | 22 ++++++++++++++-------- tests/Feature/AdminAuthorizationTest.php | 8 ++++++++ 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbf96d5..eecce5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [0.2.1] - 2026-09-22 +- The user directory, a single user, and the engagement report answer to the configured owner, as every other endpoint here already did. They were reachable by any authenticated caller + ## [0.2.0] - 2026-09-22 ### Added diff --git a/composer.json b/composer.json index 6cee31c..bcfc657 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "whilesmart/eloquent-admin", "description": "Shared measurements, user directory and automatic email templates for Laravel applications.", "type": "library", - "version": "0.2.0", + "version": "0.2.1", "license": "MIT", "authors": [{ "name": "Whilesmart Team" }], "require": { diff --git a/src/Http/Controllers/AdminController.php b/src/Http/Controllers/AdminController.php index 62fc9d5..5a21ef3 100644 --- a/src/Http/Controllers/AdminController.php +++ b/src/Http/Controllers/AdminController.php @@ -27,7 +27,7 @@ class AdminController extends Controller */ public function offers(Request $request, OfferRegistry $offers): JsonResponse { - $this->authorizeOffers($request); + $this->authorizeConsole($request); return response()->json([ 'success' => true, @@ -42,7 +42,7 @@ public function offers(Request $request, OfferRegistry $offers): JsonResponse public function createOffer(Request $request, OfferRegistry $offers, string $provider): JsonResponse { - $this->authorizeOffers($request); + $this->authorizeConsole($request); // Defaulted rather than read straight out: a host that published this // config before the key existed has an array that wins over the one @@ -56,7 +56,7 @@ public function createOffer(Request $request, OfferRegistry $offers, string $pro public function revokeOffer(Request $request, OfferRegistry $offers, string $provider, string $id): JsonResponse { - $this->authorizeOffers($request); + $this->authorizeConsole($request); $this->offerProvider($offers, $provider)->revoke($id); @@ -64,11 +64,11 @@ public function revokeOffer(Request $request, OfferRegistry $offers, string $pro } /** - * Offers answer to the configured owner, the way a mail template answers to - * its own. Checked here rather than in the request, so a host swapping the - * request class cannot drop it. + * The console answers to the configured owner, the way a mail template + * answers to its own. Checked in the controller rather than in a request + * class, so a host swapping that class cannot drop it. */ - private function authorizeOffers(Request $request): void + private function authorizeConsole(Request $request): void { $owner = config('admin.owner'); @@ -90,6 +90,8 @@ private function offerProvider(OfferRegistry $offers, string $key): OfferProvide public function metrics(Request $request, EngagementManager $engagement, ClientRegistry $clients): JsonResponse { + $this->authorizeConsole($request); + $granularity = in_array($request->query('granularity'), ['day', 'week', 'month'], true) ? $request->query('granularity') : 'day'; @@ -105,14 +107,18 @@ public function metrics(Request $request, EngagementManager $engagement, ClientR public function users(Request $request, AdminUserProvider $provider): JsonResponse { + $this->authorizeConsole($request); + $resource = config('admin.resources.user', AdminUserResource::class); $users = $provider->paginate((string) $request->input('q', ''), (int) $request->input('per_page', 25)); return response()->json(['success' => true, 'data' => $resource::collection($users)->response()->getData(true)]); } - public function user(mixed $id, AdminUserProvider $provider): JsonResponse + public function user(Request $request, mixed $id, AdminUserProvider $provider): JsonResponse { + $this->authorizeConsole($request); + $resource = config('admin.resources.user', AdminUserResource::class); $user = $provider->find($id); diff --git a/tests/Feature/AdminAuthorizationTest.php b/tests/Feature/AdminAuthorizationTest.php index 5bf9ea8..3ccc661 100644 --- a/tests/Feature/AdminAuthorizationTest.php +++ b/tests/Feature/AdminAuthorizationTest.php @@ -38,4 +38,12 @@ public function templates_are_hidden_and_writes_are_forbidden_when_authorization 'body' => 'No', ])->assertForbidden(); } + + #[Test] + public function the_directory_and_the_report_are_forbidden_when_authorization_denies(): void + { + $this->getJson('/api/admin/users')->assertForbidden(); + $this->getJson('/api/admin/users/1')->assertForbidden(); + $this->getJson('/api/admin/metrics')->assertForbidden(); + } }