Fix forum policies

This commit is contained in:
DariusIII
2026-03-09 10:00:19 +01:00
parent a7e1d684bc
commit 4a631b6262
4 changed files with 129 additions and 26 deletions
+23 -14
View File
@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User;
use App\Models\User;
use TeamTeaTime\Forum\Models\Category;
class CategoryPolicy extends \TeamTeaTime\Forum\Policies\CategoryPolicy
@@ -19,57 +19,66 @@ class CategoryPolicy extends \TeamTeaTime\Forum\Policies\CategoryPolicy
public function manageThreads(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function deleteThreads(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function restoreThreads(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function enableThreads(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function moveThreadsFrom(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function moveThreadsTo(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function lockThreads(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function pinThreads(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function view(mixed $user, Category $category): bool
{
return $user->hasAnyRole(['Admin', 'Moderator']);
return $this->canViewCategory($user);
}
public function delete(mixed $user, Category $category): bool
{
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
public function edit(User $user, Category $category): bool
public function edit(mixed $user, Category $category): bool
{
/** @var \App\Models\User $user */
return $user->hasRole('Admin');
return $this->isAdmin($user);
}
private function isAdmin(mixed $user): bool
{
return $user instanceof User && $user->hasRole('Admin');
}
private function canViewCategory(mixed $user): bool
{
return $user instanceof User && $user->hasAnyRole(['Admin', 'Moderator']);
}
}
+14 -3
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
use TeamTeaTime\Forum\Models\Post;
class PostPolicy extends \TeamTeaTime\Forum\Policies\PostPolicy
@@ -11,18 +12,28 @@ class PostPolicy extends \TeamTeaTime\Forum\Policies\PostPolicy
public function edit(mixed $user, Post $post): bool
{
// Admins can edit any post; users can edit their own
return $user->hasRole('Admin') || ($user->getKey() === $post->author_id); // @phpstan-ignore property.notFound
return $this->isAdmin($user) || $this->isPostAuthor($user, $post);
}
public function delete(mixed $user, Post $post): bool
{
// Admins can delete any post; users can delete their own
return $user->hasRole('Admin') || ($user->getKey() === $post->author_id); // @phpstan-ignore property.notFound
return $this->isAdmin($user) || $this->isPostAuthor($user, $post);
}
public function restore(mixed $user, Post $post): bool
{
// Admins can restore any post; users can restore their own
return $user->hasRole('Admin') || ($user->getKey() === $post->author_id); // @phpstan-ignore property.notFound
return $this->isAdmin($user) || $this->isPostAuthor($user, $post);
}
private function isAdmin(mixed $user): bool
{
return $user instanceof User && $user->hasRole('Admin');
}
private function isPostAuthor(mixed $user, Post $post): bool
{
return $user instanceof User && $user->getKey() === $post->author_id;
}
}
+32 -9
View File
@@ -4,55 +4,78 @@ declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
use TeamTeaTime\Forum\Models\Thread;
class ThreadPolicy extends \TeamTeaTime\Forum\Policies\ThreadPolicy
{
public function view(mixed $user, Thread $thread): bool
{
// Everyone (including Admin) can view by default
return parent::view($user, $thread);
// Guests can still view threads, but all mutating actions require auth.
return $user === null || parent::view($user, $thread);
}
public function rename(mixed $user, Thread $thread): bool
{
// Admins can rename any thread; users can rename their own
return $user->hasRole('Admin') || ($user->getKey() === $thread->author_id); // @phpstan-ignore property.notFound
return $this->isAdmin($user) || $this->isThreadAuthor($user, $thread);
}
public function reply(mixed $user, Thread $thread): bool
{
if (! $user instanceof User) {
return false;
}
// Admins can reply even if locked; otherwise respect lock state
return $user->hasRole('Admin') || (! $thread->locked);
return $this->isAdmin($user) || (! $thread->locked);
}
public function replyWithoutApproval(mixed $user, Thread $thread): bool
{
// Admins can reply to unapproved threads; otherwise only thread author
return $user->hasRole('Admin') || ($user->getKey() === $thread->author_id); // @phpstan-ignore property.notFound
return $this->isAdmin($user) || $this->isThreadAuthor($user, $thread);
}
public function delete(mixed $user, Thread $thread): bool
{
// Admins can delete any thread; users can delete their own
return $user->hasRole('Admin') || ($user->getKey() === $thread->author_id); // @phpstan-ignore property.notFound
return $this->isAdmin($user) || $this->isThreadAuthor($user, $thread);
}
public function restore(mixed $user, Thread $thread): bool
{
// Admins can restore any thread; users can restore their own
return $user->hasRole('Admin') || ($user->getKey() === $thread->author_id); // @phpstan-ignore property.notFound
return $this->isAdmin($user) || $this->isThreadAuthor($user, $thread);
}
public function deletePosts(mixed $user, Thread $thread): bool
{
if (! $user instanceof User) {
return false;
}
// Admins can delete posts in any thread; otherwise fall back to default (true)
return $user->hasRole('Admin') || parent::deletePosts($user, $thread);
return $this->isAdmin($user) || parent::deletePosts($user, $thread);
}
public function restorePosts(mixed $user, Thread $thread): bool
{
if (! $user instanceof User) {
return false;
}
// Admins can restore posts in any thread; otherwise fall back to default (true)
return $user->hasRole('Admin') || parent::restorePosts($user, $thread);
return $this->isAdmin($user) || parent::restorePosts($user, $thread);
}
private function isAdmin(mixed $user): bool
{
return $user instanceof User && $user->hasRole('Admin');
}
private function isThreadAuthor(mixed $user, Thread $thread): bool
{
return $user instanceof User && $user->getKey() === $thread->author_id;
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Tests\Unit\Policies;
use App\Policies\CategoryPolicy;
use App\Policies\PostPolicy;
use App\Policies\ThreadPolicy;
use TeamTeaTime\Forum\Models\Category;
use TeamTeaTime\Forum\Models\Post;
use TeamTeaTime\Forum\Models\Thread;
use Tests\TestCase;
final class ForumGuestPolicyTest extends TestCase
{
public function test_guest_category_actions_are_denied_without_throwing(): void
{
$policy = new CategoryPolicy;
$category = new Category;
$this->assertFalse($policy->manageThreads(null, $category));
$this->assertFalse($policy->deleteThreads(null, $category));
$this->assertFalse($policy->restoreThreads(null, $category));
$this->assertFalse($policy->lockThreads(null, $category));
$this->assertFalse($policy->pinThreads(null, $category));
$this->assertFalse($policy->moveThreadsFrom(null, $category));
$this->assertFalse($policy->view(null, $category));
}
public function test_guest_thread_actions_are_denied_but_threads_remain_viewable(): void
{
$policy = new ThreadPolicy;
$thread = new Thread([
'author_id' => 1,
'locked' => false,
]);
$this->assertTrue($policy->view(null, $thread));
$this->assertFalse($policy->rename(null, $thread));
$this->assertFalse($policy->reply(null, $thread));
$this->assertFalse($policy->replyWithoutApproval(null, $thread));
$this->assertFalse($policy->delete(null, $thread));
$this->assertFalse($policy->restore(null, $thread));
$this->assertFalse($policy->deletePosts(null, $thread));
$this->assertFalse($policy->restorePosts(null, $thread));
}
public function test_guest_post_actions_are_denied_without_throwing(): void
{
$policy = new PostPolicy;
$post = new Post([
'author_id' => 1,
]);
$this->assertFalse($policy->edit(null, $post));
$this->assertFalse($policy->delete(null, $post));
$this->assertFalse($policy->restore(null, $post));
}
}