Authorize API social accounts via SocialAccountPolicy.

Replace repeated workspace_id checks with PostPolicy-style denyAsNotFound tenancy so cross-tenant lookups stay 404 without leaking existence.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Paulo Castellano 2026-07-24 22:09:35 -03:00
parent 7b986fca9c
commit c040ba4686
3 changed files with 70 additions and 19 deletions

View file

@ -28,14 +28,9 @@ public function index(Request $request): AnonymousResourceCollection
return SocialAccountResource::collection($accounts);
}
public function toggle(Request $request, SocialAccount $account): SocialAccountResource|JsonResponse
public function toggle(Request $request, SocialAccount $account): SocialAccountResource
{
if ($account->workspace_id !== $request->user()->currentWorkspace->id) {
return response()->json(
['message' => 'Account not found.'],
Response::HTTP_NOT_FOUND,
);
}
$this->authorize('view', $account);
ToggleSocialAccount::execute($account);
@ -44,12 +39,7 @@ public function toggle(Request $request, SocialAccount $account): SocialAccountR
public function boards(Request $request, SocialAccount $account): JsonResponse
{
if ($account->workspace_id !== $request->user()->currentWorkspace->id) {
return response()->json(
['message' => 'Account not found.'],
Response::HTTP_NOT_FOUND,
);
}
$this->authorize('view', $account);
if ($account->platform !== Platform::Pinterest) {
return response()->json(
@ -77,12 +67,7 @@ public function boards(Request $request, SocialAccount $account): JsonResponse
public function channels(Request $request, SocialAccount $account): JsonResponse
{
if ($account->workspace_id !== $request->user()->currentWorkspace->id) {
return response()->json(
['message' => 'Account not found.'],
Response::HTTP_NOT_FOUND,
);
}
$this->authorize('view', $account);
if ($account->platform !== Platform::Discord) {
return response()->json(

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\SocialAccount;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class SocialAccountPolicy
{
/**
* Authorize access to a social account. Must live in the user's current
* workspace; cross-workspace lookups deny as 404 so we don't leak
* existence across tenants (same tenancy pattern as PostPolicy).
*/
public function view(User $user, SocialAccount $account): bool|Response
{
if ($account->workspace_id !== $user->current_workspace_id) {
return Response::denyAsNotFound();
}
return true;
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
use App\Enums\UserWorkspace\Role;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Workspace;
use App\Policies\SocialAccountPolicy;
use Illuminate\Auth\Access\Response;
beforeEach(function () {
$this->policy = new SocialAccountPolicy;
});
test('members of the current workspace can view a social account', function () {
$user = User::factory()->create();
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
$workspace->members()->attach($user->id, ['role' => Role::Member->value]);
$user->update(['current_workspace_id' => $workspace->id]);
$account = SocialAccount::factory()->create(['workspace_id' => $workspace->id]);
expect($this->policy->view($user->fresh(), $account))->toBeTrue();
});
test('cross-workspace social account lookups deny as not found', function () {
$user = User::factory()->create();
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
$workspace->members()->attach($user->id, ['role' => Role::Member->value]);
$user->update(['current_workspace_id' => $workspace->id]);
$otherWorkspace = Workspace::factory()->create();
$account = SocialAccount::factory()->create(['workspace_id' => $otherWorkspace->id]);
$result = $this->policy->view($user->fresh(), $account);
expect($result)->toBeInstanceOf(Response::class)
->and($result->status())->toBe(404);
});