From 3b96a9ebdb65d71eda5e6a2ef3a7a510b5ca777c Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Mon, 4 May 2026 13:20:52 -0300 Subject: [PATCH] refactor: move workspace tenancy check on Post into PostPolicy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same "is this post in the user's current workspace?" check was duplicated across every Post-related endpoint (5 in Api/PostController via the ensurePostInCurrentWorkspace helper, 5 in App/PostController inline). PostPolicy already had a duplicate() method following this exact pattern — extending it with view/update/delete unifies the tenancy guard in one place. - Add view/update/delete to PostPolicy. Each returns Response::denyAsNotFound() when the post belongs to a different workspace, so we keep the existing 404 behavior (don't leak cross-tenant existence) instead of switching to the default 403. - Update duplicate() to also use denyAsNotFound() for the workspace mismatch path. The createPost role check still returns bool/403. - Replace ensurePostInCurrentWorkspace() calls in Api/PostController with $this->authorize('view'|'update'|'delete', $post). Helper deleted. - Replace inline workspace_id !== $workspace->id checks in App/PostController (show/edit/update/destroy/platformMetrics) with the same authorize calls. The PostPolicy guard now subsumes both the workspace-tenancy check and the role-permission check that was previously delegated through Workspace::createPost. --- app/Http/Controllers/Api/PostController.php | 19 +++----- app/Http/Controllers/App/PostController.php | 32 +++---------- app/Policies/PostPolicy.php | 50 ++++++++++++++++++--- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/app/Http/Controllers/Api/PostController.php b/app/Http/Controllers/Api/PostController.php index e2f6ab17..2ef5eee8 100644 --- a/app/Http/Controllers/Api/PostController.php +++ b/app/Http/Controllers/Api/PostController.php @@ -35,7 +35,7 @@ public function index(Request $request): AnonymousResourceCollection public function show(Request $request, Post $post): PostResource { - $this->ensurePostInCurrentWorkspace($request, $post); + $this->authorize('view', $post); $post->load(['postPlatforms.socialAccount', 'user', 'labels']); @@ -59,7 +59,7 @@ public function store(StorePostRequest $request): JsonResponse public function update(UpdatePostRequest $request, Post $post): PostResource|JsonResponse { - $this->ensurePostInCurrentWorkspace($request, $post); + $this->authorize('update', $post); $result = UpdatePost::execute($request->user()->currentWorkspace, $post, $request->validated()); @@ -75,7 +75,7 @@ public function update(UpdatePostRequest $request, Post $post): PostResource|Jso public function destroy(Request $request, Post $post): JsonResponse { - $this->ensurePostInCurrentWorkspace($request, $post); + $this->authorize('delete', $post); DeletePost::execute($post); @@ -84,7 +84,7 @@ public function destroy(Request $request, Post $post): JsonResponse public function attachMedia(Request $request, Post $post): PostMediaAttachResource { - $this->ensurePostInCurrentWorkspace($request, $post); + $this->authorize('update', $post); $validated = $request->validate([ 'urls' => ['required', 'array', 'min:1', 'max:10'], @@ -100,7 +100,7 @@ public function attachMedia(Request $request, Post $post): PostMediaAttachResour public function metrics(Request $request, Post $post): PostMetricsResource { - $this->ensurePostInCurrentWorkspace($request, $post); + $this->authorize('view', $post); $post->load(['postPlatforms.socialAccount']); @@ -109,17 +109,10 @@ public function metrics(Request $request, Post $post): PostMetricsResource public function preview(Request $request, Post $post): PostPreviewResource { - $this->ensurePostInCurrentWorkspace($request, $post); + $this->authorize('view', $post); $post->load(['postPlatforms.socialAccount']); return new PostPreviewResource($post); } - - private function ensurePostInCurrentWorkspace(Request $request, Post $post): void - { - if ($post->workspace_id !== $request->user()->currentWorkspace->id) { - abort(Response::HTTP_NOT_FOUND); - } - } } diff --git a/app/Http/Controllers/App/PostController.php b/app/Http/Controllers/App/PostController.php index afeba1df..b2b62409 100644 --- a/app/Http/Controllers/App/PostController.php +++ b/app/Http/Controllers/App/PostController.php @@ -165,13 +165,7 @@ public function store(Request $request): RedirectResponse|\Symfony\Component\Htt public function platformMetrics(Request $request, Post $post, PostPlatform $postPlatform): JsonResponse { - $workspace = $request->user()->currentWorkspace; - - if (! $workspace || $post->workspace_id !== $workspace->id) { - abort(404); - } - - $this->authorize('view', $workspace); + $this->authorize('view', $post); if ($postPlatform->post_id !== $post->id) { abort(404); @@ -188,11 +182,7 @@ public function show(Request $request, Post $post): Response|RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); - - if ($post->workspace_id !== $workspace->id) { - abort(404); - } + $this->authorize('view', $post); if (in_array($post->status, [PostStatus::Draft, PostStatus::Scheduled, PostStatus::Failed], true)) { return redirect()->route('app.posts.edit', $post); @@ -214,11 +204,7 @@ public function edit(Request $request, Post $post): Response|RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('view', $workspace); - - if ($post->workspace_id !== $workspace->id) { - abort(404); - } + $this->authorize('update', $post); if (in_array($post->status, [PostStatus::Publishing, PostStatus::Published, PostStatus::PartiallyPublished], true)) { return redirect()->route('app.posts.show', $post); @@ -274,11 +260,7 @@ public function update(UpdatePostRequest $request, Post $post): RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('createPost', $workspace); - - if ($post->workspace_id !== $workspace->id) { - abort(404); - } + $this->authorize('update', $post); $result = UpdatePost::execute($workspace, $post, $request->validated()); @@ -316,11 +298,7 @@ public function destroy(Request $request, Post $post): RedirectResponse return redirect()->route('app.workspaces.create'); } - $this->authorize('createPost', $workspace); - - if ($post->workspace_id !== $workspace->id) { - abort(404); - } + $this->authorize('delete', $post); if (in_array($post->status, [PostStatus::Publishing, PostStatus::Published, PostStatus::PartiallyPublished], true)) { session()->flash('flash.banner', __('posts.flash.cannot_delete_published')); diff --git a/app/Policies/PostPolicy.php b/app/Policies/PostPolicy.php index 9c41bd74..c89f8267 100644 --- a/app/Policies/PostPolicy.php +++ b/app/Policies/PostPolicy.php @@ -6,18 +6,58 @@ use App\Models\Post; use App\Models\User; +use Illuminate\Auth\Access\Response; class PostPolicy { /** - * Authorize duplicating a post into the user's current workspace as a - * fresh draft. The post must live in the user's current workspace and - * the user must have permission to create posts there. + * Authorize viewing a post. The post must live in the user's current + * workspace; cross-workspace lookups deny as 404 so we don't leak + * existence across tenants. */ - public function duplicate(User $user, Post $post): bool + public function view(User $user, Post $post): bool|Response { if ($post->workspace_id !== $user->current_workspace_id) { - return false; + return Response::denyAsNotFound(); + } + + return true; + } + + /** + * Authorize updating a post. Same workspace-tenancy guard as `view`. + */ + public function update(User $user, Post $post): bool|Response + { + if ($post->workspace_id !== $user->current_workspace_id) { + return Response::denyAsNotFound(); + } + + return true; + } + + /** + * Authorize deleting a post. Same workspace-tenancy guard as `view`. + */ + public function delete(User $user, Post $post): bool|Response + { + if ($post->workspace_id !== $user->current_workspace_id) { + return Response::denyAsNotFound(); + } + + return true; + } + + /** + * Authorize duplicating a post into the user's current workspace as a + * fresh draft. The post must live in the user's current workspace + * (404 otherwise — tenancy guard, see `view()`) AND the user must + * have permission to create posts there (403 otherwise). + */ + public function duplicate(User $user, Post $post): bool|Response + { + if ($post->workspace_id !== $user->current_workspace_id) { + return Response::denyAsNotFound(); } return $user->can('createPost', $user->currentWorkspace);