From e62724f2cde5c897fc7a9cd75bde007aed30f388 Mon Sep 17 00:00:00 2001 From: joemeyer76 Date: Fri, 3 Jul 2026 18:18:46 -0400 Subject: [PATCH] Fix getCategoryExclusionById() excluding every category for role-only users RolesAndPermissionsSeeder grants every 'view *' permission via Role::givePermissionTo() only -- it never grants permissions directly to a user with User::givePermissionTo(). That is true for every seeded role, including Admin. User::getCategoryExclusionById() computed the allowed permission set as: $userAllowed = $user->getDirectPermissions()->pluck('name')->toArray(); $roleAllowed = $user->getAllPermissions()->pluck('name')->toArray(); $allowed = array_intersect($roleAllowed, $userAllowed); getAllPermissions() already includes permissions granted via the user's role(s), so intersecting it with getDirectPermissions() (permissions assigned directly to the user, bypassing roles) means $allowed is empty for any user whose permissions come only from their role. Since every seeded role works this way, this silently excluded every category root for every user on a fresh install, and any subsequent Newznab/Torznab API search or browse request returned zero results with no visible error. Fix: use getAllPermissions() directly, since it already reflects both role-granted and directly-granted permissions. Added a regression test (test_role_only_permissions_are_not_excluded) that mirrors the real seeder setup -- role-only permissions, nothing granted directly to the user -- to make sure this doesn't regress. --- app/Models/User.php | 12 ++++++++--- tests/Feature/UserExcludedCategoryTest.php | 23 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 6b2055596..ade71262c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1647,9 +1647,15 @@ final class User extends Authenticatable implements CanResetPasswordContract, Ha { $user = static::findOrFail($userId); - $userAllowed = $user->getDirectPermissions()->pluck('name')->toArray(); - $roleAllowed = $user->getAllPermissions()->pluck('name')->toArray(); - $allowed = array_intersect($roleAllowed, $userAllowed); + // getAllPermissions() already merges permissions granted directly to the + // user with permissions granted via their role(s). Intersecting it with + // getDirectPermissions() (as this used to do) meant that any user whose + // permissions come only from their role -- which is how every seeded + // role in RolesAndPermissionsSeeder grants them, including Admin -- ended + // up with an empty $allowed array, and therefore every category root + // excluded. That silently zeroed out browse/API results for all users + // on a fresh install. + $allowed = $user->getAllPermissions()->pluck('name')->toArray(); $categoryPermissions = [ 'view console' => 1000, diff --git a/tests/Feature/UserExcludedCategoryTest.php b/tests/Feature/UserExcludedCategoryTest.php index dd00ce143..57a865843 100644 --- a/tests/Feature/UserExcludedCategoryTest.php +++ b/tests/Feature/UserExcludedCategoryTest.php @@ -266,6 +266,29 @@ final class UserExcludedCategoryTest extends TestCase $this->assertContains(2070, $exclusions); } + public function test_role_only_permissions_are_not_excluded(): void + { + // Regression test: RolesAndPermissionsSeeder grants every view permission + // via the role only (Role::givePermissionTo), never directly to the user + // (User::givePermissionTo). getCategoryExclusionById() must honor + // role-granted permissions, not just permissions assigned directly to + // the user, or every category ends up excluded on a fresh install. + $roleOnlyUser = $this->createTestUser($this->userRole->id); + $roleOnlyUser->assignRole($this->userRole); + // Deliberately do NOT call $roleOnlyUser->givePermissionTo(...) here. + + $exclusions = User::getCategoryExclusionById($roleOnlyUser->id); + + // The role has every 'view *' permission except 'view other', so only + // the 'Other' root category (id 1) should be excluded -- nothing from + // the seeded Movies (2000) or TV (5000) root categories. + $this->assertNotContains(2030, $exclusions); + $this->assertNotContains(2040, $exclusions); + $this->assertNotContains(2070, $exclusions); + $this->assertNotContains(5030, $exclusions); + $this->assertNotContains(5040, $exclusions); + } + public function test_clearing_exclusions_works(): void { // First add exclusions